[虚拟现实] 手把手,一起开发一个基于VR的投篮球小游戏
背景今天打开落灰的VR头盔时,突然发现两年前开发的VR小游戏,故写此篇博客与大家分享当时开发的过程。本博文以交互为主,不讨论3D建模的相关知识。
<hr>项目介绍
VR投篮球玩法: 玩家通过按下控制器的Trigger(即扳机)键来拾取篮球,把篮球投入篮框里得分,距离越远分数越高,一局有10次投篮机会。
<hr>项目依赖
运行设备: HTC VIVE / HTC VIVE PRO
操作系统: 支持Mac OS 和 Windows
运行环境: SteamVR
开发环境:
引擎:Unity 2020
IDE:Visual Studio 2019
依赖的工具包:VRTK 2.0
完整工程代码: GitHub
<hr>项目实施
开始项目之前 我们需要准备:
1.篮球3D模型及贴图
2.篮球场的3D模型
3.SteamVR sdk
4.VRTK包
开发环境的搭建可以自行百度搜索。
图1
图1
<hr>思路/开发过程
为了使思路清晰简洁,我把需要实现的功能和需要用到的技术以一一对应的方式呈现。
1.捡球投球 --> 基于控制器的抓取,释放
2.场地的不同方向移动-->基于控制器的传送机制
(注:防止晕动症的发生这里使用瞬移的方式,游戏中玩家的匀速或非匀速运动都会引起不适)
3.进球计分 --> 碰撞体判断
4.球出界自动返回原点 --> 碰撞体判断
5.计算剩余球数 --> 物体三维坐标跟踪
6.游戏结束的UI交互-->Unity UI交互
<hr>篮球
首先从篮球的特性入手,篮球具有弹性,符合物理规律,所以需要给篮球Rigidbody和Collider组件,让其受地球重力影响,并与地面发生碰撞而不是穿过地面。此时的篮球还未具有弹性,需要我们手动定义。
在project面板中新建一个Physics Material材质,并设置弹力大小以及动摩擦和静摩擦力大小。并将材质赋给篮球模型。此时篮球就可以自由下落和弹起。参数设置如图所示。
图3
篮球的属性设置完成后我们可以调整篮球在场景中的位置,以便玩家进入游戏时拾取。
篮框
当玩家把篮球投进篮框时,篮框需要作出相应的处理:计算得分,并计算剩余球数。
要使篮框作出处理首先需要为篮框添加触发器,当球投入篮框时进行相应处理,否则无动作。具体流程如下:
首先,在篮框内部新建一个空的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,分别用于显示分数文字,分数,剩余球数文字,剩余球数。如图
图4
VRTK控制器
导入了VRTK包后需要对控制器进行设置。
我们定义左手控制器用于传送,右手控制器用于投篮。
所以我们为左手控制器添加如下几个组件:
左手
为右手控制器添加:
右手
属性参数全部默认即可,需要定制可自行查阅文档定制。这里也不多赘述。
<hr>后台交互
到此,我们已经准备就绪,可以开始敲后台的交互代码。
完整源码请前往GitHub下载。
我第一个实现的功能是球出界自动回到玩家面前。经过考虑为了可玩性就没有设置自动抓取。
代码分析: 要实现这个功能只需要在Update函数中不停判断球和玩家摄影机的位置即可。同时有个问题需要注意。如果单把球移到玩家面前我们会发现球还在不停弹跳。由于球具有一定的初速度,且符合物理规律,所以这个现象是正常的。我们要解决它,就将球移动时的初速度设为0,即所谓的“瞬移”。
代码片段如下:
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()函数,在里面进行相关处理:
//触发器被触发 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()函数来计算出距离,效率上差别不大。
我自己封装了一个距离计算函数,代码如下:
//计算出玩家与碰撞体之间的距离,用于统计分数 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才设为假,重新开始下一球的判断。
此部分代码如下:
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,将它显示出来。代码如下:
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前面才能触碰到。后来就采用简单粗暴的方法,当玩家重新拾取篮球时直接开始新的一局,重置分数和剩余球数。即简单又方便。代码如下:
if (isGameOver) { RestartGame(); }//重新开始游戏 public void RestartGame() { Debug.Log("游戏将在1秒后重新开始..."); Destroy(restartUI); //延时调用初始化函数 Invoke("init", 1); }
思路其实很简单,同样是用一个bool变量来存储游戏是否结束的状态,然后进行相关处理。
<hr>总结
本项目涵盖了VR交互的基础知识点,包括unity3d的基本操作、SteamVR以及VRTK sdk的使用、VR控制器的交互:抓取物体、传送、控制器事件、C Sharp编程等内容。
完整的工程文件可在GitHub上下载,以及成品都会发布在GitHub。
页:
[1]