VR交互基础学习总结-VR投篮球项目开发

通过小项目总结Unity VR半个月的学习

背景

我从零基础入门Unity VR到现在有半个月的时间,用小项目总结半个月来的学习成果。写这篇blog来与大家分享心得体会。本博文以交互为主,不讨论3D建模的相关知识。


介绍

VR投篮球玩法: 玩家通过按下控制器的Trigger(即扳机)键来拾取篮球,把篮球投入篮框里得分,距离越远分数越高,一局有10次投篮机会。


项目参数

运行设备: HTC VIVE

操作系统: 支持Mac OS 和 Windows

运行环境: SteamVR

开发环境: Unity3D & Visual Studio

配置需求: 见HTC VIVE官网

完整工程代码: GitHub


项目分析

开始项目之前 我们需要准备:

1.篮球3D模型及贴图

2.场地的3D模型

3.SteamVR sdk

4.VRTK包

开发环境就不多赘述了

篮球场地的材质贴图就不上了,模拟一下效果即可。

注: 模型资源以及场景贴图来源于Google 侵删~


开发思路&过程

为了使思路清晰简洁,我把需要实现的功能和需要用到的技术以一一对应的方式呈现。

1.捡球投球 –> 基于控制器的抓取,释放

2.场地的不同方向移动–>基于控制器的传送机制
(注:防止晕动症的发生这里使用瞬移的方式,游戏中玩家的匀速或非匀速运动都会引起不适)

3.进球计分 –> 碰撞体判断

4.球出界自动返回原点 –> 碰撞体判断

5.计算剩余球数 –> 物体三维坐标跟踪

6.游戏结束的UI交互–>Unity UI交互


篮球

首先从篮球的特性入手,篮球具有弹性,符合物理规律,所以需要给篮球Rigidbody和Collider组件,让其受地球重力影响,并与地面发生碰撞而不是穿过地面。此时的篮球还未具有弹性,需要我们手动定义。

在project面板中新建一个Physics Material材质,并设置弹力大小以及动摩擦和静摩擦力大小。并将材质赋给篮球模型。此时篮球就可以自由下落和弹起。参数设置如图所示。

篮球的属性设置完成后我们可以调整篮球在场景中的位置,以便玩家进入游戏时拾取。

篮框

当玩家把篮球投进篮框时,篮框需要作出相应的处理:计算得分,并计算剩余球数。

要使篮框作出处理首先需要为篮框添加触发器,当球投入篮框时进行相应处理,否则无动作。具体流程如下:

首先,在篮框内部新建一个空的GameObject,调整GameObject位置及尺寸,使得篮球经过篮框时会与此游戏对象发生碰撞,实现加分处理。

此GameObject无需符合物理规律,不受外力影响。不可被玩家看见,并且篮球与其发生碰撞时可以直接穿过。所以我们为其添加一个BoxCollider,编辑好Collider,使其覆盖物体外围,并在Collider属性中勾选Is Trigger,使其成为一个触发器。

最后,新建一个tag,命名为trigger,并赋给此GameObject。后面写后台交互代码时会用到。

这时,我们的篮框触发器就设置好了。

UI界面

本项目目的在于总结VR交互技术,所以把UI的权重降低了,但是UI在实际开发中是很重要的。

我们只做一个游戏结束的UI界面和游戏信息显示界面。

首先在Unity的Hierarchy面板中新建一个GameObject 命名为GameOverUI,再把我们的篮球模型复制一个进去,作为UI的子物体,接着创建3D Text 作为子物体,内容为:Game Over。调整UI位置以及大小,注意放在显眼的位置。

然后将做好的GameOverUI拖入Project面板下的Prefab文件夹中,使其成为预制体。然后删除Hierarchy面板中的GameOverUI。这样我们的游戏结束界面就做好了。

同理,我们来制作游戏信息显示界面

在Hierarchy面板右键,选择UI-Canvas新建一个Canvas,重命名为GameInfo

然后在Canvas中新建4个Text,分别用于显示分数文字,分数,剩余球数文字,剩余球数。如图

VRTK控制器

导入了VRTK包后需要对控制器进行设置。

我们定义左手控制器用于传送,右手控制器用于投篮。

所以我们为左手控制器添加如下几个组件:

为右手控制器添加:

属性参数全部默认即可,需要定制可自行查阅文档定制。这里也不多赘述。


后台交互

到此,我们已经准备就绪,可以开始敲后台的交互代码。

完整源码请前往GitHub下载。

我第一个实现的功能是球出界自动回到玩家面前。经过考虑为了可玩性就没有设置自动抓取。

代码分析: 要实现这个功能只需要在Update函数中不停判断球和玩家摄影机的位置即可。同时有个问题需要注意。如果单把球移到玩家面前我们会发现球还在不停弹跳。由于球具有一定的初速度,且符合物理规律,所以这个现象是正常的。我们要解决它,就将球移动时的初速度设为0,即所谓的“瞬移”。

代码片段如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if(transform.position.y <= -1)
{
//isForceNeed = true;
Debug.Log("球出界了");
//Destroy(gameObject);
rb = GetComponent<Rigidbody>();

//给刚体设置一个速度 确保物体落地后不再受弹力影响
rb.velocity = new Vector3(0, 0, 0);
pos = Camera.main.transform.position;
pos.y = 0;

//将物体移动到摄影机下方,方便抓取
transform.position = pos;
rb.velocity = new Vector3(0, 0, 0);
}

接着需要实现进球加分功能。这个功能很简单,直接判断篮球是否碰撞触发器即可。(这里存在一个bug,当球自下而上抛时也会加分,这个后期可以通过空间向量解决)

写一个OnTriggerEnter()函数,在里面进行相关处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//触发器被触发
private void OnTriggerEnter(Collider other)
{
Debug.Log("OnTriggerEnter触发!Tag是" + other.tag);
if (other.CompareTag("trigger") && !isGameOver)
{
//Debug.Log("球进了!");
//距离计算
Debug.Log(distanceCal(other));
//总分统计 总分=基础分+距离四舍五入分
scoreCount += basicScore + (int)Math.Round(distanceCal(other), 0);
//刷新显示
score.text = scoreCount.ToString();
}

if (isGameOver)
{
RestartGame();
}

}

这里我们的分数不是简单的+1操作。

进球的得分=基础分+距离分

所以我们需要判断玩家投球的位置离篮框有多远。思路是计算两个空间向量之间的距离。我们可以使用距离公式来实现,也可以更简单地调用Vector3类中的distance()函数来计算出距离,效率上差别不大。

我自己封装了一个距离计算函数,代码如下:

1
2
3
4
5
6
7
8
//计算出玩家与碰撞体之间的距离,用于统计分数
private float distanceCal(Collider other)
{
Vector3 v_player = Camera.main.transform.position;
Vector3 v_collider = other.transform.position;
float distance = Vector3.Distance(v_player, v_collider);
return distance;
}

这样,在进球时的得分就会根据距离改变,距离越远,分数越高。

接着我们需要来判断什么时候从剩余球数里面减一。

我最初的思路是判断控制器扳机放开的瞬间,把球数减一。但考虑到玩家可能手滑或者需要运球,这样就会导致剩余球数频繁减少,直接Game over。

所以我决定换一个思路。当球达到一定高度时剩余球数才会减少。这样可以防止手滑或者运球造成的“冤枉”。

这个地方有一个注意点:球本身是有弹性的,如果不加判断的话,落地后重新弹起也会造成剩余球数减少,球高抛在空中停留也会造成次数多次减少。所以我们只将第一次达到那个高度时的球称为有效,其他均无效。这就需要引入一个开关变量,作为标志。

定义一个bool变量,命名为ballCircle

当达到一定高度时,ballCircle为真,直到球贴地并且末速度接近0时ballCircle才设为假,重新开始下一球的判断。

此部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
rb = GetComponent<Rigidbody>();
if(transform.position.y >= 2.5f && !ballCircle)
{
totalBall--;
ball.text = totalBall.ToString();
ballCircle = true;
}
//Debug.Log("速度: x: " + rb.velocity.x + " y: " + rb.velocity.y + " z: " + rb.velocity.z);
if (rb.velocity.y > -1.0f && rb.velocity.y < 1.0f && transform.position.y < 2f)
{
ballCircle = false;
}

到此,我们的评分和计算剩余球数的功能全部完成。

最后是游戏结束界面的显示以及玩家和界面的交互。

我们前面已经将游戏结束的UI制作成prefab,到了调用它的时候了。

我们定义一个函数名称为:onGameOver(),在剩余球数为0的时候延时3秒调用它。在函数中,我们需要实例化我们制作的prefab,将它显示出来。代码如下:

1
2
3
4
5
6
7
8
9
10
11
private void onGameOver()
{
isGameOver = true;
Debug.Log("游戏结束");
//生成游戏结束UI
restartUI = Instantiate(GameOverUI);
rb = restartUI.GetComponent<Rigidbody>();
rb.angularVelocity = new Vector3(0, 2, 0);//让UI旋转,提升体验
restartUI.transform.position = new Vector3(-4, 4, 3);

}

玩家要重新开始游戏怎么办?

有很多种方法,我想到用控制器去触碰游戏结束界面来重新开始游戏,但后来想想,觉得很麻烦,玩家需要先传送到UI前面才能触碰到。后来就采用简单粗暴的方法,当玩家重新拾取篮球时直接开始新的一局,重置分数和剩余球数。即简单又方便。代码如下:

1
2
3
4
if (isGameOver)
{
RestartGame();
}
1
2
3
4
5
6
7
8
//重新开始游戏
public void RestartGame()
{
Debug.Log("游戏将在1秒后重新开始...");
Destroy(restartUI);
//延时调用初始化函数
Invoke("init", 1);
}

思路其实很简单,同样是用一个bool变量来存储游戏是否结束的状态,然后进行相关处理。

整个项目的开发思路基本就是这样的,通过这样一个小项目的开发,可以巩固VR交互的知识,在开发中会有一些坑的出现,解决掉下次就不会再次跳坑里了。

最后就是打包发布了。这部分同样不是我们的重点,这里也不详细说明了。


总结

我写这个小项目的灵感来源于对计算机的热爱以及对新知识的追求。看起来很小的一个项目实际上涵盖了VR交互的基础知识点,包括unity3d的基本操作、SteamVR以及VRTK sdk的使用、VR控制器的交互:抓取物体、传送、控制器事件、C Sharp编程等内容。可以很好地总结这阶段的学习,并在实践中获取新知。

完整的工程文件可在GitHub上下载,以及成品都会发布在GitHub。欢迎大家指出问题。