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)

最终效果:

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/80036.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

CNN(八):Inception V1算法实战与解析

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客 &#x1f356; 原作者&#xff1a;K同学啊|接辅导、项目定制 1 Inception V1 Inception v1论文 1.1 理论知识 GoogLeNet首次出现在2014年ILSVRC比赛中获得冠军。这次的版本通常称其为Inception V1。…

strncpy

strncpy&#xff1a; 函数介绍&#xff1a; 函数原型&#xff1a; char *strncpy(char *dest, const char *src, int n) 返回值&#xff1a;dest字符串起始地址 说明&#xff1a; 1、当src字符串长度小于n时&#xff0c;则拷贝完字符串后&#xff0c;剩余部分将用空字节填…

建站系列(八)--- 本地开发环境搭建(WNMP)

目录 相关系列文章前言一、准备工作二、Nginx安装三、MySQL安装四、PHP安装及Nginx配置五、总结 相关系列文章 建站系列&#xff08;一&#xff09;— 网站基本常识 建站系列&#xff08;二&#xff09;— 域名、IP地址、URL、端口详解 建站系列&#xff08;三&#xff09;— …

【设计模式】二、UML 类图概述

文章目录 常见含义含义依赖关系&#xff08;Dependence&#xff09;泛化关系&#xff08;Generalization&#xff09;实现关系&#xff08;Implementation&#xff09;关联关系&#xff08;Association&#xff09;聚合关系&#xff08;Aggregation&#xff09;组合关系&#x…

【JavaScript保姆级教程】输出函数和初识变量

文章目录 前言一、输出内容1.1 document.write()函数1.2 console.log()函数查看终端输出信息 1.3 alert()函数 二、变量的使用1.1 变量的声明1.3变量的赋值1.4 变量的声明和赋值 三、输入提示框的使用总结 前言 JavaScript是一种强大的脚本语言&#xff0c;广泛应用于网页开发…

文件批量重命名:自定义命名与扩展名更改

你是否曾经需要批量更改文件名称和类型&#xff1f;如果你有大量文件需要重命名和更改类型&#xff0c;那么今天我们将向你介绍一种简单的方法来轻松批量更改文件名称和类型。无论你是需要将一个文件夹中的所有图片改为另一种格式&#xff0c;还是需要将一个文件夹中的所有文档…

【基于多输出方向的同步异步日志系统】

本项目涉及的到所有源码见以下链接&#xff1a; https://gitee.com/ace-zhe/wz_log 一、项目简介 1.日志的概念&#xff08;白话版&#xff09; 日志类似于日记&#xff0c;通常是指对完成某件事情的过程中状态等的记录&#xff0c;而计算机中的日志是指日志数据&#xff0c…

轻松学习 Spring 事务

文章目录 一. Spring事务简介二. Spring事务使用1. 编程式事务2. 声明式事务 三. Transactional的使用1. 参数作用2. 事务失效的场景3. Transactional工作原理 四. Spring 事务的隔离级别五. Spring事务传播机制 一. Spring事务简介 在之前的博客已经介绍了在 Spring 环境中整…

结构体变量的初始化和引用

任务描述 本关任务&#xff1a;从键盘输入两个学生的学号&#xff0c;姓名和成绩&#xff08;整数&#xff09;&#xff0c;分别存入结构体中&#xff0c;输出成绩较高的学生的学号&#xff0c;姓名和成绩。 相关知识 结构体类型用于描述由多个不同数据类型的数据构成的复合…

浅析安防监控系统/AI视频智能分析算法:河道水文水位超标算法应用

传统的水位水尺刻度尺位监测中&#xff0c;所采用的人工读数方式&#xff0c;效率较为低下且 人工成本较高&#xff0c;不利于作业流程的数字化。尽管感应器检测会自动对水位的模拟输入进行筛选&#xff0c;但是由于成本、使用场景要求高、后续日常维护复杂等多种因素&#xff…

Pytorch实现图像语义分割(初体验)

Pytorch实现图像语义分割&#xff08;初体验&#xff09; 这些天在学习图像语义分割相关的知识&#xff0c;并简单写了篇概述。原本想先看几篇经典论文&#xff0c;如全卷积网络FCN&#xff0c;奈何英语水平有限&#xff0c;翻译起来实在费劲。想来不如先直接体验一下语义分割…

vscode c++解决包含头文件红色波浪线问题

安装c/c插件后&#xff0c;按ctrlshiftp&#xff0c; 点击打开了c_cpp_properties.json文件&#xff0c;对其中的IncludePath进行编辑&#xff0c;示例如下&#xff1a; "includePath": ["${workspaceFolder}/**","${workspaceFolder}/include/**&q…

Gin 打包vue或react项目输出文件到程序二进制文件

Gin 打包vue或react项目输出文件到程序二进制文件 背景解决方案1. 示例目录结构2. 有如下问题要解决:3. 方案探索 效果 背景 前后端分离已成为行业主流&#xff0c;vue或react等项目生成的文件独立在一个单独目录&#xff0c;与后端项目无关。 实际部署中&#xff0c;通常前面套…

JDK9特性——模块化REPL工具

文章目录 前言模块化模块化案例 可交互的REPL工具 前言 谈到Java9大家往往第一个想到的就是Jigsaw项目&#xff08;后改名为Modularity&#xff09;。众所周知&#xff0c;Java已经发展超过20年(95年最初发布)&#xff0c;Java和相关生态在不断丰富的同时也越来越暴露出一些问…

嵌入式入门教学——模电基础概念

目录 1、模拟信号和模拟电路 2、研究领域 3、常用术语 3.1、共价键 3.2、电场 3.3、温度的电压当量 3.4、动态信号 3.5、直流电流和交流电流 3.6、内阻 3.7、信号频率 3.8、电容 3.9、电感 3.10、相位 3.11、信号失真 3.12、电导 3.13、跨导 3.14、电位 3.15…

瑞萨MCU入门教程(非常详细的瑞萨单片机入门教程)

瑞萨MCU零基础入门系列教程 前言 得益于瑞萨强大的MCU、强大的软件开发工具(e studio)&#xff0c;也得益于瑞萨和RA生态工作室提供的支持&#xff0c;我们团队编写了《ARM嵌入式系统中面向对象的模块编程方法》&#xff0c;全书37章&#xff0c;将近500页: 讲解面向对象编程…

linux————ansible

一、认识自动化运维 自动化运维: 将日常IT运维中大量的重复性工作&#xff0c;小到简单的日常检查、配置变更和软件安装&#xff0c;大到整个变更流程的组织调度&#xff0c;由过去的手工执行转为自动化操作&#xff0c;从而减少乃至消除运维中的延迟&#xff0c;实现“零延时”…

多线程回顾、集合Collection、Set、List等基本知识

多线程回顾 问: 多线程的两种创建方式? 继承Thread类实现Runnable接口线程池Callable 问:多线程通常会遇到线程安全问题? 什么情况下会遇到线程安全问题? 答:一个数据被多个线程访问(有读有写) 解决这个问题的方式? SE:同步锁 synchronized A : 同步代码块 B : 同步方法…

VisualStudio Code 支持C++11插件配置

问题 Visual Studio Code中的插件: Code Runner 支持运行C、C、Java、JS、PHP、Python等多种语言。 但是它不支持C11特性的一些使用&#xff0c;比如类似错误&#xff1a; binarySearch.cpp:26:17: error: non-aggregate type ‘vector’ cannot be initialized with an ini…

【深度学习】 Python 和 NumPy 系列教程(十):NumPy详解:2、数组操作(索引和切片、形状操作、转置操作、拼接操作)

目录 一、前言 二、实验环境 三、NumPy 0、多维数组对象&#xff08;ndarray&#xff09; 1. 多维数组的属性 1、创建数组 2、数组操作 1. 索引和切片 a. 索引 b. 切片 2. 形状操作 a. 获取数组形状 b. 改变数组形状 c. 展平数组 3. 转置操作 a. 使用.T属性 b…