Unity XR实现交互(抓取,移动旋转,传送,射击)-Pico

Posted 窗外听轩雨

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unity XR实现交互(抓取,移动旋转,传送,射击)-Pico相关的知识,希望对你有一定的参考价值。

Unity XR实现交互(抓取,移动旋转,传送,射击)

文章目录

前言

Unity XR提供了XR Interaction Toolkit交互工具包直接在Package Manager中安装/更新即可,对于简单的交互需求可以实现0代码,只需要将相应的脚本挂载到手柄和物体身上即可。本次演示使用当前最新的2.1.0 preview版本,如果版本较低可能会有部分脚本不可用,请尽可能更新到最新的版本。

本次演示使用Pico XR SDK,在导入SDK后其会自行携带一个XR Interaction Toolkit的0.9版本,请一定从Package Manager中进行更新,否则后续的选项会有较大的区别。

PS: 更新到新版后,很多脚本都提供了新版Action-based方式,和老版Device-based,本篇博客会以Device-based进行讲解,关于Unity新版输入系统的使用,笔者会在下一篇文章着重去讲。

一、手柄的创建

  • Ray Interactor:会创建一个带有射线的手柄模板对象
  • Direct Interactor:会创建一个不具有射线的手柄模板对象
  • XR Origin(VR): 建议,会直接创建出VR摄像机+两个射线手柄对象,更为方便后续可直接在此基础上做更改。

二、手柄的抓取

1. XR DirectController 手柄直接抓取

  • Interaction Manager: 交互管理类 — 关键,一般会随着XR Origin一起创建出来,一般创建手柄会直接挂载上去,如果没有挂载需要去Hireachy面板手动挂载一下。
  • Interaction Layer Mask : 在可抓取物体身上的XR Grab Interactable脚本上也有同名属性,只有物体的Layer Mask和手柄的匹配才可被抓取。
  • Attack Transform : 手柄的抓取位置,同时物体也可以设置抓取位置,当抓取时手柄的抓取位置会和物体的抓取位置相同,根据具体需求加以配置合适的位置即可。
  • 碰撞体:如果不是使用模板配置,而是自行挂载脚本很容易忘记此组件,直接抓取必须具有碰撞体才可以检测到可抓取物体并进行抓取操作,而射线则无需碰撞体,只需射线检测。

2. XR Ray Interactor 手柄射线抓取

手柄射线抓取一般使用默认设置即可实现将击中的物体吸到手柄上,无需添加碰撞体,诸如Layer Mask等属性与上述一致,不再赘述。

3. XR Grab Interactable 可抓取物体

  • Interaction Manager/LayerMask/Attack Transform,几个常用属性在XR DirectController中描述过,不再赘述
  • Collider:必须拥有,抓取是通过碰撞器检测所实现的
  • Rigibody:建议拥有,VR场景下的可抓取物体的抓取投掷等效果均基于自带的刚体。
  • Interactable Events:里面提供了抓取物体一系列事件的回调,如抓取物体的回调,释放物体的回调,抓取物体后按下Trigger键的回调等等,后续的射击功能正是基于此实现的。

三、手柄摇杆控制移动和旋转

以左手摇杆控制持续旋转,右手摇杆控制持续移动为例

根据上图,在XR Origin上添加Locomotion System,Continous Turn Provider等组件,再按照上图箭头进行配置即可。

  • 注意这些脚本不是必须放在XR Origin身上,任意地方均可但属性必须进行配置,放在XR Origin上方便管理。
  • Continous 是持续进行移动和旋转的脚本,由手柄的摇杆控制,如果想一次旋转或移动一段距离/角度,可以用Snap Turn/Move,在低版本的XR互动包中可能会不支持某些脚本。

四、手柄射线传送

1. 基本功能版



  • 基本的传送管理类Teleportion Provider必须要添加,推荐一起添加在XR Origin中

  • 负责传送的手柄,首先必须是XR Ray Interactor射线控制,一般来讲会使用抛物线,或者贝塞尔曲线,关于其相关参数可以搜索贝塞尔曲线的知识,Sample Frequency取样点会直接影响曲线的平滑程度。

  • Reticle为射线击中目标后的指示器,很适合作为传送目标点的指示器,如果没有合适的指示器可以用球体简单演示。

除了射线手柄的设置,在需要传送的地面上还需要添加Teleportation Area脚本(如果想只能传送到固定的点可以用Anchor,用法一致),然后射线检测到可以传送的地面后射线会变为绿色,默认按下Trigger键即可实现传送。

2. 进阶版

待改进的问题

  1. 只挂载默认的脚本,有时很大程度上没法满足需求,例如一般手柄的传送功能不会一直存在曲线去选择位置,我们会希望其在按下某个按键或者推动摇杆时出现曲线旋转位置,在松开按键或摇杆时进行传送等。

  2. 负责传送的手柄无法同时挂载Ray Interactor和Direct Interactor也就是说,我们无法通过仅仅挂载脚本去实现手柄平常负责直接抓取,而想传送时得到射线选取位置传送。

针对问题1,2的场景,笔者以左手柄平常负责直接抓取,按下A键出现射线选择位置传送为例,给出一种解决方案。

基本思路

  1. 创建两个左手柄对象,一个挂载Direct Interactor负责日常抓取,一个挂载Ray Interactor负责传送。
  2. 平常Ray Interactor脚本会被禁用掉,只有当按下A键后启用可以选择位置,当松开A键后代码控制发送传送请求,再禁用掉脚本指示器等等。

关于手柄按键的按下和抬起 Down和Up事件,如果使用老的输入系统可以参考这篇文章https://blog.csdn.net/Q540670228/article/details/123139150,关于脚本的启动禁用过程较为简单,下面只给出抬起按键时发起传送请求的代码。

    //传送管理器脚本的获取省略
	public TeleportationProvider provider;
	//负责传送的手柄XRRayInteractor脚本,获取过程省略
	public XRRayInteractor teleportRayInteractor;
	private void LeftAXButtonUpHandler()
    
        //射线信息
        RaycastHit rayInfo;
        //判断目标点是否有效 1.是否击中目标 2.是否具有Teleportation组件
        bool isValid = teleportRayInteractor.TryGetCurrent3DRaycastHit(out rayInfo);
        isValid = isValid && rayInfo.collider != null &&
                  (rayInfo.collider.GetComponent<TeleportationArea>() != null ||
                   rayInfo.collider.GetComponent<TeleportationAnchor>() != null);
        if (isValid)
        
            //创建传送需求将射线击中的位置作为目的地,提交到provider中的传送队列。
            TeleportRequest request = new TeleportRequest();
            request.destinationPosition = rayInfo.point;
            provider.QueueTeleportRequest(request);
        
        
        //禁用相关脚本和指示器
        teleportRayInteractor.enabled = false;
        teleportRayInteractor.GetComponent<XRInteractorLineVisual>().reticle?.SetActive(false);
    

五、手柄控制射击

在VR游戏中经常会有捡起枪进行射击的功能,其一般设计难点在

  1. 判断物体是否被抓取,被哪个手柄抓取
  2. 抓取了此物体的手柄是否按下了Trigger键

在XR Grab Interactable脚本中已经很贴心的提供了一个事件,触发条件正是上述这两个。使用方式如下

private XRGrabInteractable grab;
private void Awake()

    grab = GetComponent<XRGrabInteractable>();
    //为activated事件添加监听函数即可
    grab.activated.AddListener(GrabHandler);


private void GrabHandler(ActivateEventArgs a)

	//当枪械被抓取和按下Trigger后回调此方法,触发射击方法。
    Shoot();

Unity Pico Neo3 基础开发流程

Unity Pico Neo3 基础开发流程

Pico 基础模块

Pico 开发者平台

链接: PICO 开发者平台
链接: PICO 文档中心
链接: Pico GitHub
链接: PicoXR SDK 官方存储库

如果是第一次进入需要先注册成为开发者。
然后下载SDK

这个是 Unity相关的 SDK
注意下载的平台,并下载最新版。

我这里下载的是:PICO UnityXR Integration SDK v207。

解压: 最有用的就是这个 package.json 文件。

这个是 实时预览 和 Pico Neo3 串流工具。

解压之后的文件夹状态:
再把 PreviewTool_0402_Release.7z 解压。
PreviewTool_0402.apk : 这个文件是 Pico Neo3 的安装包

在用 USB 数据线 连接到 PicoNeo3 把PreviewTool_0402.apk 文件复制到内部存储器中。
打开 PicoNeo3 并安装

下载 Android Debug Bridge(ADB) 调试工具包。

链接: Android Debug Bridge(ADB)

如果无法下载就尝试一下 下面这个链接。

链接: Android Debug Bridge(ADB) CSDN

Pico 管理中心

在 Pico 开发者平台 点击管理中心

进行账号密码登录,没有的话就注册一个

登录之后 点击创建一个新的应用

创建应用时最好 选择 6DOF

创建完毕之后,点击刚刚创建好的应用。

点击 当前应用 API

这里的 API 就是后期发布应用时会用到的。

Pico 实时预览 测试

1. 使用 USB 数据线连接 PICO VR 一体机与 PC
2. 使用 Win + R 输入 cmd 打开 命令行工具  

3. 在命令行窗口中 输出:cd D:\\Unity\\Plug-in\\Pico SDK\\platform-tools_r33.0.3-windows\\Android Debug Bridge(ADB再次输入:d:

4. 状态检查 输入:adb devices。
  (若第一次执行该命令,此时 PICO VR 一体机屏幕上会通过对话框提示 “是否允许USB Debugger”,点击 同意。)
  (命令执行后,如果出现设备序列号,代表连接成功。)
  (这个序列号等会要在 Unity 中使用。)

5. 在PicoNeo3 中点击文件管理->安装包->PreviewTool_0402.apk 进行PreviewTool安装。

6.***\\PicoPreviewTool V1.0-0402\\PreviewTool_0402_Release\\Release 文件夹下 双击打开 PreviewTool.exe

7. 两种链接模式:
   无线连接 :需保证 PCVR 一体机处于同一 Wi-Fi 环境下。
   (推荐) 有线连接 :需使用 USB 数据线连接 PCVR 一体机。

链接成功状态。

8. 在PicoNeo3打开  PreviewTool 工具

9. 选择有线连接 连接完毕之后,整个画面就会变黑。
   点击 Unity 编辑器界面顶部的 播放 按钮。
   VR 一体机上就会呈现与 PC 同步的场景画面。

Unity 模块

链接: 配置开发环境 Pico 官方文档

项目创建

1. Unity 编辑器(Unity Editor)须使用 2019.4.0 及以上版本。(因为要使用 XR 模块)

2. 先进行安卓平台 切换。

切换后的状态。

3. 打开 Package Manager。

4. 点击 Add package from disk

5. 选择 package.json 文件并导入。

6. 下载 XR Interaction Toolkit 并下载 Starter Assets、XR Device Simulator、Tunneling Vignette。

7. 在 App ID 字段处,填入应用 ID。点击 Apply。

8. 在安卓平台 勾选 PicoXR

9. 在微软平台 勾选 PicoXR

10. Other Settings 标签,在 Identification 设置区域:
   Minimum API Level (最低API级别)更改为 API level 27。
   Target API Level 设置为 Automatic (highest installed)

11. 在 Configuration 设置区域:
    Scripting Backend 设置为 IL2CPP。
    Target Architectures 设置为 ARM64 ,并取消勾选 ARMv7。

12. 在菜单栏 点击 PXR_SDK -> Platform Setting -> Authorization Check Simulation(授权检查模拟)

13. 添加设备 SN 码 就是在命令行工具中 获取到的那个 机器码

基础XR 模块

1. 在 Hierarchy窗口 右键 -> XR -> XR origin

2. 添加组件:
   XR_Origin:原点
   PXR_Manager:PXR 管理
   TeleportationProvider:传送
   LocomotionSystem:运动系统
   InputActionManager:输入操作
   DeviceBasedSnapTurnProvider:基于设备的快速转向   

3. 在 Hierarchy 窗口选中 LeftHand Controller\\RightHand Controller
   剔除XRController(Action-based)组件。

4. 添加 XRController(Device-based)组件。
   RightHand Controller 也一样。

5. 注意在 XRController(Device-based)组件上添加 Model 模型,不然就会只有射线。
   RightHand Controller 也一样。
   Model 模型在:Packages/Pico intergration/Assets/Resources/Prefabs 文件夹下

6. XR Interactor Line Visual:射线风格化
   更改 Width Curve 风格化:下行线 的效果是 手柄宽,射线终点窄。
   Reticle:射线终点,如果不设置就是单纯的射线,如果设置就是你设置的模型。
   我这里是使用的 Sphere 小球,大小 0.01f。

传送 模块

在 Hierarchy 窗口 创建一个 Plane 把Transform组件 Reset一下。
并添加 TeleportationArea:传送区域 组件。

锚点传送 模块

在 Hierarchy 窗口 创建一个 Plane 更改名字(改不改都行)
再在此物体下创建一个空物体作为锚点位置(那个蓝色的小框)。
在 Plane 上添加 TeleportationAnchor:锚点传送 组件
并把锚点位置 空物体赋予给 Telepor Anchor Transform(锚点物体)

射线抓取 模块

在 Hierarchy 窗口 创建一个 Sphere 更改名字(改不改都行)
在 Sphere 上添加 XR Grb Interactable:抓取 组件

手柄碰撞抓取 模块

和上面的差不多
在 Hierarchy 窗口 创建一个 Cube 更改名字(改不改都行)
在 Cube上添加 XR Grb Interactable:抓取 组件。
区别就是 注意添加手柄碰撞抓取。

当然手柄也要做出响应的变化:
把你需要响应的手只保留 XR Controller(Device-based)组件。
并添加 XR Direct InterActor 和 Capsule Collider 组件。
Capsule Collider 组件的 Radius 和 Height 设置为 0.1f。

XR UI模块

在Hierarchy 窗口 右键 XR -> UI Canvas 进行 UI 模块创建。

为什么要在 XR 的模块下创建 UI 呢? 他和普通的 UI 差别在哪里呢?
首先使用 常规 UI 创建画布 那么 XR模式先就无法响应。
其次 XR 模块下创建的 UI 画布 带了一个TrackedDeviceGraphicRaycaster(跟踪设备图形射线)组件
也就是这个组件接管了 普通 UI 画布的射线管理系统,要是没有这个组件 那么所有的 UI 事件都无法响应。

EventSystem 事件系统:
和普通 事件系统的差异化就是 XRUIInputModule(UI 输入 模块)原本的是 StandaloneInputModule组件。
如果你发现你的UI 射线无法点击也无法响应,就查看一下是不是这里出了问题。

射线碰撞事件响应 模块

在 Hierarchy 窗口 创建一个 Cube 更改名字(改不改都行)
在 Cube上添加 XR SImple Interactable:响应组件(不添加也行,不添加就代码添加)。

响应并实现:
(SelectEnterEventArgs Data) =>  OnPointerClick(Data); 
可直接省略为 (Data) =>  OnPointerClick(Data); 
 void Start()
    
        GameObject.Find("射线碰撞").GetComponent<XRSimpleInteractable>().selectEntered.AddListener((SelectEnterEventArgs Data) =>  OnPointerClick(Data); );
        GameObject.Find("射线碰撞").GetComponent<XRSimpleInteractable>().selectExited.AddListener((SelectExitEventArgs Data) =>  OnPointerClick(Data); );
        GameObject.Find("射线碰撞").GetComponent<XRSimpleInteractable>().hoverEntered.AddListener((HoverEnterEventArgs Data) =>  OnPointerClick(Data); );
        GameObject.Find("射线碰撞").GetComponent<XRSimpleInteractable>().hoverExited.AddListener((HoverExitEventArgs Data) =>  OnPointerClick( Data); );

        //GameObject.Find("射线碰撞").GetComponent<XRSimpleInteractable>().selectEntered.AddListener((Data) =>  OnPointerClick(Data); );
        //GameObject.Find("射线碰撞").GetComponent<XRSimpleInteractable>().selectExited.AddListener((Data) =>  OnPointerClick(Data); );
        //GameObject.Find("射线碰撞").GetComponent<XRSimpleInteractable>().hoverEntered.AddListener((Data) =>  OnPointerClick(Data); );
        //GameObject.Find("射线碰撞").GetComponent<XRSimpleInteractable>().hoverExited.AddListener((Data) =>  OnPointerClick(Data); );

    

       /// <summary>
    /// 射线进入并按键
    /// </summary>
    /// <param name="Data"></param>
    public void OnPointerClick(SelectEnterEventArgs Data)
    
        GameObject.Find("Text").GetComponent<Text>().text = "射线进入并按键";
    

    /// <summary>
    /// 射线退出松开按键
    /// </summary>
    /// <param name="Data"></param>
    public void OnPointerClick(SelectExitEventArgs Data)
    
        GameObject.Find("Text").GetComponent<Text>().text = "射线进入松开按键";
    

    /// <summary>
    /// 射线悬停
    /// </summary>
    /// <param name="Data"></param>
    public void OnPointerClick(HoverEnterEventArgs Data)
    
        GameObject.Find("Text").GetComponent<Text>().text = "射线悬停";
    

    /// <summary>
    /// 射线悬停退出
    /// </summary>
    /// <param name="Data"></param>
    public void OnPointerClick(HoverExitEventArgs Data)
    
        GameObject.Find("Text").GetComponent<Text>().text = "射线悬停退出";
    

Pico XR 健值操作

using System.Collections;
using System.Collections.Generic;
using Unity.XR.PXR;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.XR;
using UnityEngine.XR.Interaction.Toolkit;
/// <summary>
/// XR 健值操作
/// </summary>
public class PicoKeysOperation_ZH : MonoBehaviour

    [Header("左手控制器")]
    public XRController _LeftController;
    [Header("右手控制器")]
    public XRController _RightController;

    //摇杆移动输出值
    private Vector2 _Result;
    //移动物体
    private Transform _TargetTra;
    void Start()
    
        _TargetTra = GameObject.Find("摇杆移动").transform;

        GameObject.Find("射线碰撞").GetComponent<XRSimpleInteractable>().selectEntered.AddListener((Data) =>  OnPointerClick(Data); );
        GameObject.Find("射线碰撞").GetComponent<XRSimpleInteractable>().selectExited.AddListener((Data) =>  OnPointerClick(Data); );
        GameObject.Find("射线碰撞").GetComponent<XRSimpleInteractable>().hoverEntered.AddListener((Data) =>  OnPointerClick(Data); );
        GameObject.Find("射线碰撞").GetComponent<XRSimpleInteractable>().hoverExited.AddListener((Data) =>  OnPointerClick(Data); );
    

    /// <summary>
    /// 射线进入并按键
    /// </summary>
    /// <param name="Data"></param>
    public void OnPointerClick(SelectEnterEventArgs Data)
    
        GameObject.Find("Text").GetComponent<Text>().text = "射线进入并按键";
    

    /// <summary>
    /// 射线退出松开按键
    /// </summary>
    /// <param name="Data"></param>
    public void OnPointerClick(SelectExitEventArgs Data)
    
        GameObject.Find("Text").GetComponent<Text>().text = "射线进入松开按键";
    

    /// <summary>
    /// 射线悬停
    /// </summary>
    /// <param name="Data"></param>
    public void OnPointerClick(HoverEnterEventArgs Data)
    
        GameObject.Find("Text").GetComponent<Text>().text = "射线悬停";
    

    /// <summary>
    /// 射线悬停退出
    /// </summary>
    /// <param name="Data"></param>
    public void OnPointerClick(HoverExitEventArgs Data)
    
        GameObject.Find("Text").GetComponent<Text>().text = "射线悬停退出";
    

    void Update()
    
        //是否成功返回
        //获取手部控制器的 摇杆值
        var _Success = _LeftController.inputDevice.TryGetFeatureValue(CommonUsages.primary2DAxis, out _Result);
        if (_Success)
        
            //物体移动
            _TargetTra.position = new Vector3(_TargetTra.position.x + _Result.x * Time.deltaTime, _TargetTra.position.y, _TargetTra.position.z + _Result.y * Time.deltaTime);
        


        // X键 按下
        if (_LeftController.inputDevice.TryGetFeatureValue(CommonUsages.primaryButton, out bool _PrimaryBool))
        
            if (_PrimaryBool)
            
                GameObject.Find("Text").GetComponent<Text>().text = "左手 按下 primaryButton X键";
            

        
        // A键 按下
        if (_RightController.inputDevice.TryGetFeatureValue(CommonUsages.primaryButton, out bool _RightPrimaryBool))
        
            if (_RightPrimaryBool)
            
                GameObject.Find("Text").GetComponent<Text>().text = "右手按下 primaryButton A键";
            
        
        //Y键 按下
        if (_LeftController.inputDevice.TryGetFeatureValue(CommonUsages.secondaryButton, out bool _SecondaryBool))
        
            if (_SecondaryBool)
            
                GameObject.Find("Text").GetComponent<Text>().text = "按下 secondaryButton Y键";
            

        
        //B键 按下
        if (_RightController.inputDevice.TryGetFeatureValue(CommonUsages.secondaryButton, out bool _RightSecondaryBool))
        
            if (_RightSecondaryBool)
            
                GameObject.Find("Text").GetComponent<Text>().text = "按下 secondaryButton B键";
            

        


        //握柄键
        if (_LeftController.inputDevice.TryGetFeatureValue(CommonUsages.grip, out float _Value))
        
            if (_Value > 0.8f)
            
                //Debug.LogWarning($"握柄键按下:_Value");
                GameObject.Find("Text").GetComponent<Text>().text = $"Grip键 按下:_Value";
            
        
        //握柄键 按下
        if (_LeftController.inputDevice.TryGetFeatureValue(CommonUsages.gripButton, out bool _GripBool))
        

            if (_GripBool)
            
                //Debug.LogWarning("按下握柄键");
                GameObject.Find("Text").GetComponent<Text>().text = "按下握柄键";
            
        


        //扳机键
        if (_LeftController.inputDevice.TryGetFeatureValue(CommonUsages.trigger, out float _TriggerValue))
        
            if (_TriggerValue > 0.8f)
            
                //Debug.LogWarning($"扳机键按下:_TriggerValue");
                GameObject.Find("Text").GetComponent<Text>().text = $"Trigger键 按下:_TriggerValue";
            
        
        //扳机键按下
        if (_LeftController.inputDevice.TryGetFeatureValue(CommonUsages.triggerButton, out bool _TriggerBool))
        
            if (_TriggerBool)
            
                //Debug.LogWarning("按下扳机键");
                GameObject.Find("Text").GetComponent<Text>().text = "按下 Trigger键";
            
        

        //菜单键按下
        if (_LeftController.inputDevice.TryGetFeatureValue(CommonUsages.menuButton, out bool _MenuBool))
        
            以上是关于Unity XR实现交互(抓取,移动旋转,传送,射击)-Pico的主要内容,如果未能解决你的问题,请参考以下文章

Unity VR开发教程 OpenXR+XR Interaction Toolkit 传送

Unity Pico Neo3 基础开发流程

unity物体缓慢停止

Unity开发OpenXR |使用 OpenXR 添加一个运动系统,实现传送抓取功能 的简单VR示例场景 的全过程详细教程

Unity VR开发教程 OpenXR+XR Interaction Toolkit UI

XR Interaction Toolkit教程丨实现与UI交互