前言
在oculus quest2里的first step应用里体验了多种有趣的交互,如乒乓球,遥控飞艇,拉线火箭,纸飞机等。其中尤以纸飞机给我的手感最为特殊(VR纸飞机哪里来的手感啊喂,还不是有需求就做喽)。这里就大概记录一下其间的开发历程。
一、准备工作
1.引入插件
steamvr plugin2.x的版本对输入都做了单独的隔离,可以很方便地适应多种输入设备和多重输入模式的开发。同时,其中提供的基础脚本组件也十分完备且可理解(Hand,Throwable,Laser等等),在它基础上可以很快进行开发且拓展性良好。但是相对的steamvr plugin不兼容VRTK(VR tool kit,不再更行了,时代的眼泪),对习惯了VRTK的人来讲不得不说是一种遗憾。
具体操作:再asset store里搜索steamvr plugin,下载导入,接受所有的弹框,完毕后,找到window -> steamvr input,点击。
保持默认,点击save and generate。
基本的开发环境就布置好了,打开Interaction_Example场景,里面依旧存放了一些制作好的交互物体了。
2.准备模型
拖入一个cube作为放置纸飞机的台子。
随便找一个纸飞机的模型,导入到unity,拖到场景中,并赋予合适材质,如图:
对飞机模型进行简要处理:目的是保持z轴正方向和飞机的朝向一致,做法就是建一个根节点,并将飞机模型拖入作为子节点,并适当调整方向。
OK,到这里准备工作基本完成。下面看看我们到底要干什么,该怎么干。
二、需求拆解
那么扔纸飞机到底是一个怎样的过程呢?你要用自己的虚拟手(左手右手其中一只)“捏”起飞机,然后扔出,飞机通过手的带动获得一个初速度(包含大小和方向),以与手脱离的点为初始点做一个平抛的运动。整体过程基本上就是这样,是不是感觉回到了高中基本物理。
当然,学过大学物理,尤其是流体力学的小伙伴都明白,纸飞机依照构造不同,抛出角度和速度的不同,其轨迹千差万别,千奇百怪(例如,纸飞机在空中的突然急转,原因是因为飞机的结构以及空气流体的影响)。我尝试了去构建纸飞机在空气流体中运动的理论模型,把压强、对流等都考虑了进去,做了很多这方面的研究后发现(其实是模型越搞越混乱,笔者只是大学上过一点力学的小白),其实这陷入到了一个误区:在游戏引擎里去追求真实的力学仿真。花这么大力气去“模拟”现实,不如把精力用在改善用户体验上(“超脱”现实),遂放弃。
用户体验又从两方面可以入手――视觉反馈和力反馈,对应头显和手柄。视觉上,抛物线要尽量柔美,不能太过剧烈,同时需要一些视觉辅助(如拖尾和一些其他特效等)。力反馈的话,主要是体现在手柄的震动上,即要让使用者在抓握到纸飞机时体会到与什么都没抓握到时不一样的感觉。
总结下来,需求部分如下:
- 纸飞机抓握,扔出
- 纸飞机扔出时轨迹是抛物线
- 纸飞机飞行特效
- 抓握纸飞机的手柄反馈
三、实现部分
1.纸飞机抓握,扔出
在开发之前,我们先要了解一下Player这个预制体(prefab)。可以理解为它包含了我们的vr camera和两只手并处理其输入响应,负责将这三者映射到虚拟环境中(当然还有一些其他的组件,例如负责输入模式调控的InputModule,debug组件等)。简单来说,将这个场景中有了这个prefab之后,你戴上头显启动应用,就可以进入到虚拟环境中通过头显看到虚拟环境中的物体以及自己的虚拟双手了。
回到我们之前构建好的场景(Interactions_Example)中,此时物体上没有挂任何脚本,所以暂时没有任何交互效果。
为PAP(我们纸飞机的根节点)添加碰撞体组件(我选用了BoxCollider)和Interactable脚本(steamvr plugin里的),并适当调节碰撞体:
此时,启动应用进入虚拟环境,发现手在进入模型碰撞体后,已经可以高亮模型了。
但是这还不够,试着去抓握的话,物体并没有与手的有效互动,此时需要添加另一个脚本组件Throwable(也是steamvr plugin里的)。添加了这一脚本后,会自动为物体添加刚体Rigidbody组件。此时,再次进入虚拟环境,并尝试抓握纸飞机:
纸飞机可以被顺利抓起并且丢出,但是此时也发现,在抓握纸飞机的瞬间,手部模型消失不见。我们详细看一下Interactable脚本组件:
其中bool量 HideHandOnAttach表示在被抓握时隐藏手部模型,取消勾选。
嗯~好像还是不太对,手的姿势奇奇怪怪的,那么就k一个手势上去好了。添加SteamVR_Skeleton_Poser脚本。
点击Create创建对应姿势文件,并找一个合适的文件夹存入。而相应的,编辑器内,模型根节点下,会多出两个手部模型的克隆文件,如图:
所谓k姿势,就是去摆弄下面两个手部模型,“k”到合适的姿势,在触发交互操作时,手部会自动使用对应的姿势。记得k好姿势后,点击“Save Pose”。
好像像那么点样子了,我就不去仔细k它的样子了,这个手势可以随时回来微调,但是一定要记得保存,我们进入虚拟环境看一下:
效果还可以,那么这部分基本就完成了,我们借助了steamvr plugin提供的Interactable和Throwable组件实现了交互,并通过它提供的组件去k了手势。
当然,这两个脚本内涵丰富,光是其提供的功能就可以让我们实现很多不同的效果,你可以自行去调试切换,如切换Throwable里AttachmentFlags里面的内容,和Interactable中AttachEaseIn的开关等。
2.抛物线
因为使用了Rigidbody,即Unity的物理模块,所以扔出去的物体很自然是一个弧线(抛物线)。但是仍然有一些问题待解决:现在扔出去纸飞机就像扔出去一个砖块一样,没有办法保持飞机尖儿冲向运动方向;重力感觉太强,没有体现出空气浮力的感觉。
针对这两点,我们写脚本解决:添加自定义脚本,名称为PAPController,如下
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PAPController : MonoBehaviour
{
[Range(0,9.81f)]
public float buoyancy = 8f;
private Hand _hand;
private Interactable _interactable;
private Transform paperAirplane;
private Rigidbody _rigidbody;
private ConstantForce _constantForce;
// Start is called before the first frame update
private void Awake()
{
paperAirplane = GetComponent<Transform>();
_rigidbody = GetComponent<Rigidbody>();
_constantForce = GetComponent<ConstantForce>();
_interactable = GetComponent<Interactable>();
}
private void Start()
{
//添加浮力
_constantForce.force = new Vector3(0, buoyancy, 0);
}
// Update is called once per frame
void FixedUpdate()
{
//每帧将方向矫正为速度方向
paperAirplane.forward = _rigidbody.velocity.normalized;
}
}
添加ConstantForce组件
现在可以试试丢出去的手感啦。
3.特效
这里的特效我们主要做拖尾的特效,借助的是unity自带的TrailRenderer这个组件
可以分别做几个不同的拖尾(基于颜色,持续时间,甚至于shader的不同),
效果:
4.手柄力反馈
力反馈在Hand脚本里,有一个专门的封装好的函数可以使用,即TriggerHapticPulse。
具体简单的调用为:
private void Update()
{
if (!_interactable.attachedToHand)
{
return;
}
if (_hand != _interactable.attachedToHand)
{
_hand = _interactable.attachedToHand;
}
_hand.TriggerHapticPulse(0);
}
参数可以具体调节,同样的参数视硬件不同反馈强度也会有一定的不同。
最后再做一点点修饰(加了状态切换)(从我写的下一个脚本开始我就开始写注释,我发四)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Valve.VR;
using Valve.VR.InteractionSystem;
public class PAPController : MonoBehaviour
{
[Range(0,9.81f)]
public float buoyancy = 8f;
private enum PAPStat { InAir,InHand,ShouldbeStatic};
private PAPStat stat = PAPStat.ShouldbeStatic;
private Hand _hand;
private Interactable _interactable;
private Transform paperAirplane;
private Rigidbody _rigidbody;
private ConstantForce _constantForce;
private void Awake()
{
paperAirplane = GetComponent<Transform>();
_rigidbody = GetComponent<Rigidbody>();
_constantForce = GetComponent<ConstantForce>();
_interactable = GetComponent<Interactable>();
}
private void Start()
{
_constantForce.force = new Vector3(0, buoyancy, 0);
}
void FixedUpdate()
{
if (stat == PAPStat.InAir )
{
paperAirplane.forward = _rigidbody.velocity.normalized;
}
}
private void Update()
{
if (!_interactable.attachedToHand)
{
return;
}
if (_hand != _interactable.attachedToHand)
{
_hand = _interactable.attachedToHand;
}
_hand.TriggerHapticPulse(0);
}
public void ChangeStatToInAir()
{
if (stat != PAPStat.InAir)
{
stat = PAPStat.InAir;
}
}
public void OnCollisionEnter(Collision collision)
{
if (collision.transform.tag != "Player")
{
stat = PAPStat.ShouldbeStatic;
}
}
}
总结
本文展示了笔者从灵感思路,到编程实现的全过程,功能和代码结构当然还是有待完善和优化的地方,但是这种面对问题和解决问题的方式我认为还是有可取之处的。
对于我自身来说,写文章的目的既是希望能够对自己做的东西有个清晰的认识,同时也希望能够将这些微不足道的经验进行总结分享,帮助到更多跟我处于同一阶段的人。