当前位置: 首页 > news >正文

Unity SteamVR 开发教程:用摇杆/触摸板控制人物持续移动(2.x 以上版本)

文章目录

  • 📕教程说明
  • 📕场景搭建
  • 📕创建移动的动作
  • 📕移动脚本
    • ⭐移动
    • ⭐实时调整 CharacterController 的高度
  • 📕取消手部和 CharacterController 的碰撞

持续移动是 VR 开发中的一个常用功能。一般是用户推动手柄摇杆,或者触摸手柄触摸板,来控制人物持续地移动。Unity SteamVR 插件中只提供了传送的移动功能,而没有用摇杆或触摸板控制人物持续移动的功能。因此,持续移动的功能需要我们自己开发。


📕教程说明

使用的 Unity 版本: 2021.3.5

使用的操作系统:Windows 11

使用的设备:Meta Quest 2

SteamVR 版本:2.7.3

因为我用的是 Quest 手柄,所以我会用 Quest 手柄的摇杆控制人物移动。而像 Htc Vive 手柄上只有触摸板 Touchpad,但是它的作用和摇杆是一样的。

最终实现的效果:能通过摇杆控制人物持续移动,人物能与其他物体有碰撞,能上阶梯。

在这里插入图片描述


📕场景搭建

我们新建一个场景,删除原本场景中的 Main Camera,在场景中放置一个平面,然后用方块制作一个阶梯,用于后续的测试。接下来,我们需要添加一个 VR 中代表玩家自己的物体。我可以打开 Assets/SteamVR/InteractionSystem/Core 文件夹,将 Player 物体拖入场景:

在这里插入图片描述

在这里插入图片描述

也许你之前会在 Assets/SteamVR/Prefabs 文件夹下看到一个 [CameraRig] 预制体,它也能代表 VR 中的玩家自己,能够追踪头显和手柄的位置和旋转角度。但是 Player 这个预制体相当于功能更加丰富的 [CameraRig],因此,推荐大家使用 Player 这个预制体。


📕创建移动的动作

SteamVR 的输入系统是基于动作的,我们需要在代码中判断动作是否发生,或者获取动作返回的值,然后在配置文件中配置动作和设备按键的绑定关系,输入系统的详细介绍可以参考这篇教程:Unity SteamVR 开发教程:SteamVR Input 输入系统(2.x 以上版本)。

我们可以打开 Unity 编辑器菜单栏的 Windows/SteamVR Input 窗口:

在这里插入图片描述

然后点击 Open Binding UI 打开动作按键绑定界面:

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

我们在场景中默认会激活 default 这个动作集。然而这个动作集里并没有绑定摇杆相关的动作。因此,我们需要自己创建一个摇杆移动相关的动作,并且将它与摇杆键进行绑定。

在 VR 游戏中,经常是一只手控制移动,另一只手控制转向。转向一般是将手柄摇杆向左或者向右推动来触发。SteamVR 默认的输入配置是两只手都能转向,因此,我们需要取消其中一只手的转向绑定。那么我规定一下,在本篇教程中,我使用左摇杆进行持续移动,右摇杆进行转向。

首先,我们需要取消勾选镜像模式,这样可以为左右手柄分别绑定动作。

在这里插入图片描述

然后删除左手柄上 snapturnright 和 snapturnleft 的按键绑定:

在这里插入图片描述

接下来,我们要在 default 动作集下创建一个新的动作,用来表示我们的摇杆移动。回到 SteamVR Input 窗口,新键一个 Vector2 类型的动作。因为摇杆推向的位置是用一个二维向量来表示。

在这里插入图片描述

然后打开 Binding UI,将 MovePlayer 动作与左摇杆位置进行绑定(记得取消镜像模式)。

在这里插入图片描述

现在,我们的 Vector2 类型的动作就和左手柄摇杆绑定成功了。


📕移动脚本

我们新建一个脚本,然后挂载到 Player 物体上:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Valve.VR;
using Valve.VR.InteractionSystem;public class ContinousMoveController : MonoBehaviour
{[SerializeField] private SteamVR_Action_Vector2 moveAction;[SerializeField] private float speed = 1;[SerializeField] private float gravity = 9.81f;[SerializeField] private float minHeight = 0;[SerializeField] private float maxHeight = float.PositiveInfinity;[SerializeField] private CharacterController characterController;void Start() {if(characterController == null){characterController = GetComponent<CharacterController>();  }}void Update(){HandleHeight();Move();}private void Move(){if(moveAction.axis.magnitude > 0.1f){Vector3 direction = Player.instance.hmdTransform.TransformDirection(new Vector3(moveAction.axis.x, 0, moveAction.axis.y));            characterController.Move(speed * Time.deltaTime * Vector3.ProjectOnPlane(direction, Vector3.up) - new Vector3(0, gravity, 0) * Time.deltaTime);}}private void HandleHeight(){       float headHeight = Mathf.Clamp(Player.instance.hmdTransform.position.y, minHeight, maxHeight);characterController.height = headHeight;Vector3 newCenter = Player.instance.transform.InverseTransformPoint(Player.instance.hmdTransform.position);newCenter.y = characterController.height / 2 + characterController.skinWidth;characterController.center = newCenter;}
}

然后将 CharacterController 组件挂载到 Player 物体上,参数设置可以参考我的,大家也可以根据实际情况进行调整:

在这里插入图片描述

⭐移动

解释一下核心方法 Move:

首先我们人物的移动选择用 CharactrrController 碰撞体的 Move 方法来移动,因为人物添加上 CharactrrController 后,可以与场景中的其他物体发生碰撞,比如碰到一堵墙会卡住,或者拥有上台阶的功能。这样身体在移动的过程中就不会穿过其他碰撞体。并且 CharacterController 组件拥有一个 Move 方法,可以用于移动身体。

Move 方法需要传入一个移动向量。因为移动相当于一段距离,所以移动的距离可以看作移动速度×移动时间×移动方向。移动方向可以通过以下代码获取:

 Vector3 direction = Player.instance.hmdTransform.TransformDirection(new Vector3(moveAction.axis.x, 0, moveAction.axis.y));  

Player 就是原本挂载在 Player 物体上的 Player 脚本:

在这里插入图片描述

通过 Player.instance.hmdTransform 可以获取头部相机的位置。TransformDirection 方法可以将相对于指定对象自身坐标系的方向向量转换为相对于世界坐标系方向向量,即一个物体自己坐标系的 direction 方向,相当于世界坐标系的什么方向。我们的持续移动功能一般是相对于自己的头部移动,因为移动的输入原本是一个世界坐标系下的向量,我们需要用 TransformDirection 方法将移动输入转换为相对于玩家头部的方向,其结果是用世界坐标系下的一个向量来表示。比如头部看向右边,然后向前推动摇杆,这时候人物要朝着头部看向的右侧移动,大家可以参考下图理解。

在这里插入图片描述

但是因为我们要限制玩家在水平面上前后左右移动,而头看向的方向可以是任一方向,所以我们要用以下代码对移动方向进行限制:

Vector3.ProjectOnPlane(direction, Vector3.up)

这个方法可以将将向量投影到由法线定义的平面上(法线与该平面正交),因此我们就把移动方向限制在了水平面上。ProjectOnPlane 方法的第二个参数是平面的法向量,所以用 Vector3.up 表示合适。

在 CharacterController 的 Move 方法中,我们传入的移动向量减去了一个 new Vector3(0, gravity, 0) * Time.deltaTime,这是为了模拟重力,让玩家在能够从高处落下。

脚本写完后,我们需要在 Inspector 面板中对变量进行赋值:

在这里插入图片描述

⭐实时调整 CharacterController 的高度

移动脚本中还有个 HandleHeight 方法,这个方法用于实时调整 CharacterController 的高度。如果没有这个方法,我们的 CharacterController 的高度是不变的,比如游戏中有一个比较矮的洞,按照现实生活中的常识,我们可能会蹲下来走过去,但是游戏中人物的碰撞由 CharacterController 决定,如果我们在现实中蹲下而游戏中的 CharacterController 高度不变的话,我们在游戏中还是过不了洞,人物会因为 CharacterController 的高度卡在洞外。

因此我们希望比如在现实中蹲下,游戏中人物的 CharacterController 的高度会随之变低。也就是 CharacterController 的高度会随着头部相机的高度变化而变化

private void HandleHeight(){       float headHeight = Mathf.Clamp(Player.instance.hmdTransform.position.y, minHeight, maxHeight);characterController.height = headHeight;Vector3 newCenter = Player.instance.transform.InverseTransformPoint(Player.instance.hmdTransform.position);newCenter.y = characterController.height / 2 + characterController.skinWidth;characterController.center = newCenter;}

首先将 CharacterController 的高度设为与头部相机一样,最小高度和最大高度可以自己在 Inspector 面板定义。

但是因为调整 CharacterController 的高度会让 CharacterController 的两头都进行伸缩,所以还要调整它的 center 让它与人物匹配。

首先我们要让 CharacterController 的中心位置和头部相机位置匹配。因为此时的 CharacterController 的 center 是相对于玩家角色的局部坐标,所以我们也要得到头部相机相对于玩家角色的局部坐标,利用 InverseTransformPoint 方法将头部相机的位置从世界坐标系转换为相对于玩家角色的局部坐标系。然后调整 center 的高度,相当于头部相机高度的一半加上 CharacterController 的 skinWidth。skinWidth表示角色控制器的皮肤宽度。皮肤宽度是一个用于处理碰撞的边界区域,加上后 center 的高度更加准确。

在这里插入图片描述

可以看到 CharacterController 的高度和中心位置会在游戏运行过程中根据头部相机的位置而改变。


📕取消手部和 CharacterController 的碰撞

如果这时候运行代码,你会发现手部和身体(CharacterController)产生了碰撞。为了解决这个问题,我们可以把 Player 这个物体的 Layer 设为 Player(或者任意一个你喜欢的名字)

在这里插入图片描述

只需要将挂载了 CharacterController 的物体的 Layer 设为 Player 就行。

在这里插入图片描述

然后在 Assets/SteamVR/InteractionSystem/Core/Prefabs 文件夹下找到 HandColliderLeft 和 HandColliderRight 预制体。这两个物体会在游戏运行的时候自动创建,作为手部的碰撞体。

在这里插入图片描述

游戏运行时自动添加:

在这里插入图片描述

这两个物体的实例化是在什么时候进行的呢?我们可以找到 Player 物体的子物体 LeftHand 或者 RightHand,这两个物体上挂载了 HandPhysics 脚本:

在这里插入图片描述

在这里插入图片描述

我们可以打开 HandPhysics 脚本,看看源码:

在这里插入图片描述

可以看到在 Start 方法中就通过 Instantiate 方法实例化了 Hand Collider 的 Prefab 预制体。

现在,我们将手部碰撞体的预制体(HandColliderLeft 和 HandColliderRight)的 Layer 设为 Hand(随便取一个名字就行),这次需要选择 Yes,因为手部碰撞体所有的子物体都不能与 CharacterController 发生碰撞。

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

在这里插入图片描述

然后打开编辑器菜单栏的 Edit/Project Settings:

在这里插入图片描述

找到 Physics,取消勾选 Player 和 Hand 之间的交叉点:

在这里插入图片描述

这样,Layer 是 Player 的物体和 Layer 是 Hand 的物体就不会发生碰撞,也就是我们手部的碰撞体不会与 CharacterController 发生碰撞。我们为手部碰撞体单独设置一个 Hand Layer 是因为我们希望手与手之间可以发生碰撞,但是手和 CharacterController 不能发生碰撞。

但是这个时候如果运行程序,你可能会发现一个问题:有时候移动或者转向时手部模型会有抖动的现象。

在这里插入图片描述

这个时候我们需要找到 HandColliderLeft 和 HandColliderRight 预制体,取消勾选 Rigidbody 组件上的 Use gravity,然后将 Interpolate 选项设为 Interpolate

在这里插入图片描述

重要的是这个 Interpolate 插值选项。它可以平滑消除固定帧率运行物理导致的现象。因为在 Unity 中,物理的计算通常是以固定的时间步长进行更新,然后应用的实际帧率是不一定的。当物理计算和应用实际帧率不同步时,可能会导致对象出现视觉抖动。而 Interpolate 插值运算可以一定程度上解决抖动问题。(Unity 官方说明:https://docs.unity.cn/2023.2/Documentation/Manual/rigidbody-interpolation.html)

最终效果:

在这里插入图片描述

相关文章:

Unity SteamVR 开发教程:用摇杆/触摸板控制人物持续移动(2.x 以上版本)

文章目录 &#x1f4d5;教程说明&#x1f4d5;场景搭建&#x1f4d5;创建移动的动作&#x1f4d5;移动脚本⭐移动⭐实时调整 CharacterController 的高度 &#x1f4d5;取消手部和 CharacterController 的碰撞 持续移动是 VR 开发中的一个常用功能。一般是用户推动手柄摇杆&…...

04条件构造器和常用接口

条件构造器和常用接口 wapper介绍 条件构造器的两个条件之间默认就是AND并列关系,如果需要或者的关系则需要调用构造器的or()方法 条件构造器类型作用Wrapper条件构造抽象类,最顶端父类AbstractWrapper生成SQL的where条件QueryWrapper封装查询或删除的条件UpdateWrapper封装修…...

什么是HTTP状态码?常见的HTTP状态码有哪些?

聚沙成塔每天进步一点点 ⭐ 专栏简介⭐ 什么是HTTP状态码&#xff1f;⭐ 1xx - 信息性状态码⭐ 2xx - 成功状态码⭐ 3xx - 重定向状态码⭐ 4xx - 客户端错误状态码⭐ 5xx - 服务器错误状态码⭐ 写在最后 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 欢迎来到前…...

vue3的双向绑定原理分析

谈到vue3的双向绑定原理&#xff0c;就得先知道&#xff0c;为什么vue2的双向绑定方式会被废弃&#xff1f; vue2的双向绑定 Object.defineProperty Object.defineProperty() 方法会直接在一个对象上定义一个新属性&#xff0c;或者修改一个对象的现有属性&#xff0c;并返回…...

MySQL数据库时间计算的用法

今天给大家分享如何通过MySQL内置函数实现时间的转换和计算&#xff0c;在工作当中&#xff0c;测试人员经常需要查询数据库表的日期时间&#xff0c;但发现开发人员存入数据库表的形式都是时间戳形式&#xff0c;不利于测试人员查看&#xff0c;测试人员只能利用工具对时间戳进…...

应用在儿童平板防蓝光中的LED防蓝光灯珠

现在电子产品多&#xff0c;手机、平板电脑、电子书等等&#xff0c;由于蓝光有害眼睛健康&#xff0c;于是市场上有很多防蓝光的眼镜、防蓝光的手机膜、防蓝光的平板&#xff0c;这些材料和设备到底有没有用&#xff1f;如何正确预防蓝光危害呢&#xff1f; 我们现在所用的灯…...

BERT 快速理解——思路简单描述

定义&#xff1a; BERT&#xff08;Bidirectional Encoder Representations from Transformers&#xff09;是一种预训练的语言模型&#xff0c;它基于Transformer架构&#xff0c;通过在大规模的未标记文本上进行训练来学习通用的语言表示。 输入 在BERT中&#xff0c;输入…...

二叉树实现的相关函数

1.二叉树的创建 BTNode* BinaryTreeCreate(BTDataType* a, int n, int* pi) { if (n0||a[*pi] #){ (*pi);return NULL;}BTNode* root (BTNode*)malloc(sizeof(BTNode));root->_data a[(*pi)];root->_left BinaryTreeCreate(a, --n, pi);root->_right Binary…...

Redis面试题(二)

文章目录 前言一、Redis 支持的 Java 客户端都有哪些&#xff1f;官方推荐用哪个&#xff1f;二、Redis 和 Redisson 有什么关系&#xff1f;三、Jedis 与 Redisson 对比有什么优缺点&#xff1f;四、说说 Redis 哈希槽的概念&#xff1f;五、Redis 集群的主从复制模型是怎样的…...

STP介绍

目录 STP概述 二层环路带来的问题 1.广播风暴 2.MAC地址漂移问题 3.多帧复制---这个好理解&#xff0c;同一个数据帧被重复收到多次&#xff0c;被称为多帧复制。 802.1D生成树 STP的BPDU BPDU主要分为两大类 配置BPDU RPC COST 配置BPDU的工作过程 TCN BPDU TCN…...

numpy 和 tensorflow 中的各种乘法(点乘和矩阵乘)

嗨喽&#xff0c;大家好呀~这里是爱看美女的茜茜呐 &#x1f447; &#x1f447; &#x1f447; 更多精彩机密、教程&#xff0c;尽在下方&#xff0c;赶紧点击了解吧~ python源码、视频教程、插件安装教程、资料我都准备好了&#xff0c;直接在文末名片自取就可 点乘和矩阵乘…...

(图论) 1020. 飞地的数量 ——【Leetcode每日一题】

❓ 1020. 飞地的数量 难度&#xff1a;中等 给你一个大小为 m x n 的二进制矩阵 grid &#xff0c;其中 0 表示一个 海洋单元格、1 表示一个 陆地单元格。 一次 移动 是指从一个陆地单元格走到另一个相邻&#xff08;上、下、左、右&#xff09;的陆地单元格或跨过 grid 的边…...

c++ 重载、重写、覆盖

重载&#xff1a;指在同一作用域内&#xff0c;有多个同名但参数不同的函数的现象&#xff0c;叫重载&#xff1b;可以是任何用户定义的函数&#xff0c;例如 类成员函数、类静态函数、普通函数重写&#xff1a;子类重写父类的同名函数&#xff0c;只要子类出现有父类的同名函数…...

Python异步编程高并发执行爬虫采集,用回调函数解析响应

一、问题&#xff1a;当发送API请求&#xff0c;读写数据库任务较重时&#xff0c;程序运行效率急剧下降。 异步技术是Python编程中对提升性能非常重要的一项技术。在实际应用&#xff0c;经常面临对外发送网络请求&#xff0c;调用外部接口&#xff0c;或者不断更新数据库或文…...

SpriteKit与Swift配合:打造您的第一个简易RPG游戏的步骤指南

1. 简介&#xff1a; RPG&#xff08;Role-Playing Game&#xff09;游戏是一种角色扮演游戏&#xff0c;它允许玩家在一个虚拟的游戏世界中扮演一个或多个角色。在本教程中&#xff0c;我们将使用Apple的2D游戏框架SpriteKit和Swift编程语言来创建一个简单的RPG游戏。我们将从…...

服务网格的面临挑战:探讨服务网格实施中可能遇到的问题和解决方案

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…...

leetcode61 旋转链表

题目 给你一个链表的头节点 head &#xff0c;旋转链表&#xff0c;将链表每个节点向右移动 k 个位置。 示例 输入&#xff1a;head [1,2,3,4,5], k 2 输出&#xff1a;[4,5,1,2,3] 解析 这道题属实不好想&#xff1a;需要计算出链表的长度&#xff0c;然后在k > n的…...

【学习笔记】各类基于决策单调性的dp优化

文章目录 对于决策单调性的一般解释关于决策单调性的证明四边形不等式一维dp区间dp一种二维dp一些满足四边形不等式的函数类 与图形相结合 决策单调性的常见优化手段二分队列二分栈分治类莫队做法 SMAWKWQS二分WQS多解情况满足四边形不等式的序列划分问题的答案凸性以及WQS二分…...

【C++】构造函数初始化列表 ⑤ ( 匿名对象 生命周期 | 构造函数 中 不能调用 构造函数 )

文章目录 一、匿名对象 生命周期1、匿名对象 生命周期 说明2、代码示例 - 匿名对象 生命周期 二、构造函数 中调用 构造函数1、构造函数 中 不能调用 构造函数2、代码示例 - 构造函数中调用构造函数 构造函数初始化列表 总结 : 初始化列表 可以 为 类的 成员变量 提供初始值 ;…...

Knife4j系列--使用方法

原文网址&#xff1a;Knife4j系列--使用/教程/实例/配置_IT利刃出鞘的博客-CSDN博客...

前端倒计时误差!

提示:记录工作中遇到的需求及解决办法 文章目录 前言一、误差从何而来?二、五大解决方案1. 动态校准法(基础版)2. Web Worker 计时3. 服务器时间同步4. Performance API 高精度计时5. 页面可见性API优化三、生产环境最佳实践四、终极解决方案架构前言 前几天听说公司某个项…...

2.Vue编写一个app

1.src中重要的组成 1.1main.ts // 引入createApp用于创建应用 import { createApp } from "vue"; // 引用App根组件 import App from ./App.vue;createApp(App).mount(#app)1.2 App.vue 其中要写三种标签 <template> <!--html--> </template>…...

376. Wiggle Subsequence

376. Wiggle Subsequence 代码 class Solution { public:int wiggleMaxLength(vector<int>& nums) {int n nums.size();int res 1;int prediff 0;int curdiff 0;for(int i 0;i < n-1;i){curdiff nums[i1] - nums[i];if( (prediff > 0 && curdif…...

DIY|Mac 搭建 ESP-IDF 开发环境及编译小智 AI

前一阵子在百度 AI 开发者大会上&#xff0c;看到基于小智 AI DIY 玩具的演示&#xff0c;感觉有点意思&#xff0c;想着自己也来试试。 如果只是想烧录现成的固件&#xff0c;乐鑫官方除了提供了 Windows 版本的 Flash 下载工具 之外&#xff0c;还提供了基于网页版的 ESP LA…...

成都鼎讯硬核科技!雷达目标与干扰模拟器,以卓越性能制胜电磁频谱战

在现代战争中&#xff0c;电磁频谱已成为继陆、海、空、天之后的 “第五维战场”&#xff0c;雷达作为电磁频谱领域的关键装备&#xff0c;其干扰与抗干扰能力的较量&#xff0c;直接影响着战争的胜负走向。由成都鼎讯科技匠心打造的雷达目标与干扰模拟器&#xff0c;凭借数字射…...

uniapp中使用aixos 报错

问题&#xff1a; 在uniapp中使用aixos&#xff0c;运行后报如下错误&#xff1a; AxiosError: There is no suitable adapter to dispatch the request since : - adapter xhr is not supported by the environment - adapter http is not available in the build 解决方案&…...

如何在网页里填写 PDF 表格?

有时候&#xff0c;你可能希望用户能在你的网站上填写 PDF 表单。然而&#xff0c;这件事并不简单&#xff0c;因为 PDF 并不是一种原生的网页格式。虽然浏览器可以显示 PDF 文件&#xff0c;但原生并不支持编辑或填写它们。更糟的是&#xff0c;如果你想收集表单数据&#xff…...

如何更改默认 Crontab 编辑器 ?

在 Linux 领域中&#xff0c;crontab 是您可能经常遇到的一个术语。这个实用程序在类 unix 操作系统上可用&#xff0c;用于调度在预定义时间和间隔自动执行的任务。这对管理员和高级用户非常有益&#xff0c;允许他们自动执行各种系统任务。 编辑 Crontab 文件通常使用文本编…...

Caliper 负载(Workload)详细解析

Caliper 负载(Workload)详细解析 负载(Workload)是 Caliper 性能测试的核心部分,它定义了测试期间要执行的具体合约调用行为和交易模式。下面我将全面深入地讲解负载的各个方面。 一、负载模块基本结构 一个典型的负载模块(如 workload.js)包含以下基本结构: use strict;/…...

毫米波雷达基础理论(3D+4D)

3D、4D毫米波雷达基础知识及厂商选型 PreView : https://mp.weixin.qq.com/s/bQkju4r6med7I3TBGJI_bQ 1. FMCW毫米波雷达基础知识 主要参考博文&#xff1a; 一文入门汽车毫米波雷达基本原理 &#xff1a;https://mp.weixin.qq.com/s/_EN7A5lKcz2Eh8dLnjE19w 毫米波雷达基础…...