找回密码
 立即注册
查看: 438|回复: 0

[虚拟现实] 手把手,一起开发一个基于VR的投篮球小游戏

[复制链接]
发表于 2022-2-9 13:35 | 显示全部楼层 |阅读模式
背景

今天打开落灰的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。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-16 20:55 , Processed in 0.089952 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表