简介:这份资源是面向高校学生与Unity初学者的VR期末团队作业完整方案,以HTC Vive为运行设备,实现了一款限时射击水果的虚拟现实小游戏。玩家需用弓箭射落从地面升起、挂着不同水果的气球,按水果种类计分,玩法覆盖各年龄段,可直接作为虚拟现实课程大作业或Unity VR入门练手项目。压缩包为zip格式,整体约300.11MB,内含Unity工程源码、导出的exe可执行文件、设计文档与演示视频,源码便于二次修改,exe可在HTC设备上直接运行验证,设计文档与录屏则帮助理解场景搭建、交互逻辑与计分机制。该资源已在教室HTC设备上实测可正常运行,目前已有2720人浏览学习,适合需要完整赛题方案、可运行Demo与排错参考的团队参考借鉴。
1. 从一份 Unity VR 射击水果作业说起:为什么值得动手做
如果你正在找一份能跑起来的 Unity VR 期末作业,或者想用一套完整源码把「VR 交互 + 射击判定 + 打包 exe」这条链路走通,那这个方向比想象中更值得投入。它不是一个只能看不能改的演示包,而是一个把 XR 交互、物理碰撞、UI 计分、场景管理和 Windows 打包串起来的完整小项目。射击水果这个玩法本身门槛不高,但恰好覆盖了 VR 开发里最容易翻车的几个点:手柄射线怎么发、水果被击中后怎么碎、分数怎么同步到 UI、打包成 exe 后为什么手柄突然失灵。很多人第一次做 VR 作业,卡的不是玩法设计,而是环境配置和打包环节。这篇笔记就按「先跑通、再改懂、最后能讲清楚」的顺序,把一套可复现的 Unity VR 射击水果方案拆开讲,源码结构、关键参数、导出 exe 的注意点都会落到具体操作上。适合正在赶期末作业的学生,也适合想快速验证 VR 交互原型的开发者。
2. 环境搭起来:Unity 版本、XR 插件与 Pico4 设备链路
2.1 为什么优先选 Unity 2022 LTS + XR Interaction Toolkit
做 VR 项目,版本选错后面全是玄学。我一般会锁定 Unity 2022.3 LTS,原因是这个版本对 XR Interaction Toolkit 的支持最稳,OpenXR 插件兼容性也好,导出 Windows exe 时不容易出现手柄输入丢失。热词里提到的「unity安装」「unity下载」其实指向同一个问题:装哪个版本。不要追最新 Tech 流,期末作业求的是能跑、能打包、能演示。
核心插件组合如下:
| 插件 | 作用 | 安装方式 |
|---|---|---|
| XR Interaction Toolkit | 提供射线、抓取、传送等交互组件 | Package Manager 搜索安装 |
| OpenXR Plugin | 统一各 VR 设备的运行时接口 | Package Manager 搜索安装 |
| XR Plugin Management | 管理不同平台的 XR 加载 | 随 OpenXR 自动带入 |
安装顺序建议先装 XR Plugin Management,再装 OpenXR,最后装 XR Interaction Toolkit。顺序反了容易出现 Input System 冲突,表现是手柄按键全部无响应。
2.2 从新建工程到 Pico4 能识别的最小配置
新建 3D Core 工程后,第一步不是急着拖模型,而是把 XR 环境激活。打开 Edit > Project Settings > XR Plug-in Management,勾选 OpenXR。然后在 OpenXR 的 Feature Groups 里确认 Interaction Profile 包含你用的手柄类型。Pico4 开发 Unity 时,常见做法是安装 Pico 官方的 XR SDK,但如果你只是做 Windows exe 演示,用 OpenXR 通用链路也能跑通。
# 如果你用命令行做 CI 构建,可以用这段检查 XR 是否启用 # 在 Unity 工程根目录执行,查看 ProjectSettings 里的 XR 配置 grep -r "OpenXR" ProjectSettings/ | head -20这段命令的作用是快速确认 OpenXR 是否已经写入工程配置。如果输出为空,说明 XR 插件没启用成功,需要回到 Editor 里重新勾选。参数上重点看ProjectSettings/XRPackageSettings.asset这个文件,它记录了当前激活的 XR 加载器。
接下来配置 Input System。XR Interaction Toolkit 的新版本默认依赖 Input System,如果工程里还是旧 Input Manager,手柄射线会拿不到值。在 Player Settings > Active Input Handling 里选 Both 或 Input System Package。选 Both 最省事,但打包体积会略大。
2.3 场景里必须放的三件套:XR Origin、交互管理器、射线
一个能跑的最小 VR 场景,层级里至少要有这三个对象:
- XR Origin:替代旧版 Camera Rig,负责把头显和手柄的位置映射到场景里。
- XR Interaction Manager:管理所有交互事件,没有它射线打出去也没反应。
- Locomotion System:如果你需要传送移动,就挂上它;纯站桩射击可以不加。
XR Origin 下面会自动生成 Camera Offset、Main Camera、LeftHand Controller、RightHand Controller。检查 Main Camera 的 Tracked Pose Driver 是否绑定到 Center Eye。手柄控制器上要加 XR Controller 组件和 Input Action Manager。很多人到这里发现射线不出现,八成是 Input Action Manager 没挂或者 Action Asset 没赋值。
提示:Pico4 通过串流助手连 Unity Editor 时,如果手柄在 Scene 视图里不动,先检查 OpenXR 的 Interaction Profile 是否勾选了 Pico 对应的手柄型号。没有官方 Profile 时,选 Oculus Touch Controller Profile 通常也能兼容大部分按键。
3. 射击水果的核心逻辑:射线发射、命中判定与水果碎裂
3.1 用 XR Ray Interactor 做射击射线的三个必调参数
射击水果的「射击」本质是一条从手柄发出的射线,命中水果后触发销毁和计分。XR Interaction Toolkit 自带 XR Ray Interactor,但默认配置是抓取用的,改成射击需要调三个地方。
第一,Ray Interactor 的 Line Type 选 Straight Line,不要用 Projectile Curve。曲线适合传送,直线才像枪口。第二,Interaction Layer Mask 只勾选 Fruit 层,避免射线打到地面或 UI。第三,Max Time 设为 0,表示射线常驻,不需要按住才显示。
// ShootRayController.cs // 挂载在手柄控制器上,负责发射射线并处理命中 using UnityEngine; using UnityEngine.XR.Interaction.Toolkit; public class ShootRayController : MonoBehaviour { [SerializeField] private XRRayInteractor rayInteractor; [SerializeField] private InputActionProperty triggerAction; [SerializeField] private GameManager gameManager; private void OnEnable() { triggerAction.action.performed += OnTriggerPressed; triggerAction.action.Enable(); } private void OnDisable() { triggerAction.action.performed -= OnTriggerPressed; triggerAction.action.Disable(); } private void OnTriggerPressed(UnityEngine.InputSystem.InputAction.CallbackContext ctx) { // 用射线检测获取当前命中的第一个物体 if (rayInteractor.TryGetCurrent3DRaycastHit(out RaycastHit hit)) { FruitTarget fruit = hit.collider.GetComponent<FruitTarget>(); if (fruit != null) { fruit.OnHit(); gameManager.AddScore(fruit.scoreValue); } } } }这段代码的逻辑是:监听扳机按键,按下时从 XR Ray Interactor 取当前命中结果,如果命中物体带 FruitTarget 组件,就调用命中方法并加分。参数上,rayInteractor必须在 Inspector 里拖入同一个物体上的 XR Ray Interactor 组件;triggerAction绑定到 XR Controller 的 Activate Action;gameManager是场景里的计分管理器。容易翻车的点是TryGetCurrent3DRaycastHit在射线没命中任何东西时返回 false,如果不做判空直接取 hit,会报空引用。
3.2 水果被击中后的碎裂:粒子、音效与销毁时机
水果碎裂不要直接 Destroy,否则音效和粒子还没播完物体就没了。常见做法是命中后先禁用 Collider 和 Renderer,播放粒子与音效,延迟 0.5 秒再销毁。
// FruitTarget.cs using UnityEngine; using System.Collections; public class FruitTarget : MonoBehaviour { public int scoreValue = 10; [SerializeField] private GameObject sliceEffect; [SerializeField] private AudioClip hitSound; [SerializeField] private float destroyDelay = 0.5f; private bool isHit = false; public void OnHit() { if (isHit) return; // 防止一帧内多次触发 isHit = true; GetComponent<Collider>().enabled = false; GetComponent<MeshRenderer>().enabled = false; if (sliceEffect != null) Instantiate(sliceEffect, transform.position, Quaternion.identity); if (hitSound != null) AudioSource.PlayClipAtPoint(hitSound, transform.position); StartCoroutine(DestroyAfterDelay()); } private IEnumerator DestroyAfterDelay() { yield return new WaitForSeconds(destroyDelay); Destroy(gameObject); } }isHit这个布尔量是血泪经验:VR 里手柄抖动可能导致同一帧内射线多次命中,不加锁会重复加分。destroyDelay默认 0.5 秒,如果粒子时长是 1 秒,就改成 1 秒。sliceEffect建议用 Unity 自带的 Particle System 做一个简单的球形扩散,不要用太重的特效,VR 里掉帧比画面丑更致命。
3.3 计分 UI 与游戏循环:从 ScriptableObject 到场景重开
计分不要每个水果单独找 UI 文本,用一个 GameManager 统一管理。分数变化时更新 UI,达到目标分数后显示重开按钮。
// GameManager.cs using UnityEngine; using TMPro; public class GameManager : MonoBehaviour { public static GameManager Instance; [SerializeField] private TextMeshProUGUI scoreText; [SerializeField] private GameObject restartPanel; [SerializeField] private int targetScore = 100; private int currentScore = 0; private void Awake() { Instance = this; } public void AddScore(int value) { currentScore += value; scoreText.text = $"Score: {currentScore}"; if (currentScore >= targetScore) { restartPanel.SetActive(true); } } public void RestartGame() { UnityEngine.SceneManagement.SceneManager.LoadScene( UnityEngine.SceneManagement.SceneManager.GetActiveScene().buildIndex); } }targetScore设成 100 是给演示留余地,水果分值 10 的话打中 10 个就结束。restartPanel是一个 World Space 的 Canvas,挂在摄像机前方 2 米处,VR 里不要用 Screen Space Overlay,否则在头显里看不到。重开用LoadScene最稳,不要手动重置每个水果的位置,容易漏。
4. 导出 exe 与真机演示:打包设置、性能取舍与常见故障
4.1 Windows exe 打包的五个关键设置
导出 exe 是期末作业交付的硬指标。File > Build Settings 里选 Windows x86_64,然后重点检查这几项:
| 设置项 | 推荐值 | 原因 |
|---|---|---|
| Architecture | x86_64 | 64 位系统兼容 |
| Compression Method | LZ4HC | 加载快,体积可接受 |
| Development Build | 演示时勾选 | 方便看日志 |
| Script Debugging | 演示时勾选 | 出错能定位 |
| Graphics API | Direct3D11 | VR 兼容性最好 |
Graphics API 不要选 Auto,Auto 在某些机器上会走 Vulkan,导致头显画面撕裂。手动指定 Direct3D11 最稳。如果热词里提到的「vr渲染器切换cpu gpu模式」让你困惑,在 Unity 里对应的是 Player Settings > Rendering 下的 Graphics API 列表,把 D3D11 拖到第一位即可。
4.2 打包后手柄失灵的排查顺序
打包 exe 后手柄没反应,按这个顺序查:
- 确认 OpenXR Runtime 是否安装。Windows 上需要装对应的 OpenXR Runtime,否则 exe 启动后找不到设备。
- 检查 exe 是否以管理员权限运行。热词里「unity is running with administrator privileges」提示过这个坑,某些设备在管理员权限下反而无法读取手柄输入。
- 看 Player.log。路径在
C:\Users\用户名\AppData\LocalLow\公司名\产品名\Player.log,搜「XR」或「Input」关键字。
# 快速查看 Player.log 里的 XR 相关报错 # Windows 下用 PowerShell Get-Content "$env:LOCALAPPDATA\..\LocalLow\DefaultCompany\FruitShooter\Player.log" | Select-String "XR|Input|OpenXR"这段命令帮你过滤日志里的 XR 和输入相关行。如果看到「No XR display subsystem found」,说明 OpenXR Runtime 没装好;如果看到「Input System is not enabled」,说明打包时 Active Input Handling 选错了。
4.3 性能优化:VR 里必须守住的帧率底线
VR 项目低于 72 帧就会晕,射击水果虽然场景简单,但粒子特效和实时阴影是两大杀手。我一般会做三件事:关掉实时阴影改用烘焙、粒子最大数量限制在 50 以内、水果模型面数控制在 500 面以下。热词里的「unity游戏优化」在 VR 场景里优先级最高,因为帧率直接决定可用性。
在 Quality Settings 里把 Shadow Distance 设为 0 或很小值,Anti Aliasing 用 2x 或 4x,不要开 8x。如果用的是 Pico4 一体机模式,还要在 Pico 开发者后台里把 CPU 和 GPU 等级调到最高,否则默认会降频。
5. 避坑与排查:VR 射击水果作业里最容易翻车的五件事
5.1 射线打不中水果,但手柄明明有震动
现象:扣扳机手柄有震动反馈,但水果不消失,分数也不变。
原因:XR Ray Interactor 的 Interaction Layer Mask 没有勾选 Fruit 层,或者水果的 Collider 被禁用。另一种可能是射线被手柄自身的 Collider 挡住,射线起点在模型内部。
解决:把水果统一放到 Fruit 层,Ray Interactor 的 Layer Mask 只勾 Fruit。手柄控制器的 Collider 设为 Ignore Raycast,或者把射线起点偏移到手柄前方 0.1 米。
5.2 打包后 UI 在头显里看不见
现象:Editor 里计分文本正常,导出 exe 后头显里一片空白。
原因:Canvas 用了 Screen Space Overlay,VR 渲染模式下 Overlay 不会显示在头显里。
解决:Canvas 的 Render Mode 改成 World Space,缩放设为 0.01,位置放在摄像机前方 2 米、视线下方 0.3 米处。TextMeshPro 的字体材质要选 Mobile 或 SDF,不要用普通字体。
5.3 水果被击中后重复加分
现象:打一个水果加了 30 分,实际分值只有 10。
原因:射线在单帧内多次命中同一个 Collider,或者水果的 OnHit 被多个脚本调用。
解决:在 FruitTarget 里加isHit布尔锁,如 3.2 节代码所示。同时检查 XR Ray Interactor 的 Raycast Trigger Interaction 是否设为 Collide,不要用 Overlap。
5.4 导出 exe 时报错「Could not find OpenXR Runtime」
现象:Build 成功,但双击 exe 弹出错误框,提示找不到 OpenXR Runtime。
原因:目标机器没装 OpenXR Runtime,或者 Runtime 版本与打包时不一致。
解决:在目标机器上安装对应头显的 OpenXR Runtime。如果是 Pico,装 Pico 的串流助手和 XR Runtime;如果是 Quest,装 Oculus PC 客户端。演示前一定在目标机器上先跑一遍,不要只在开发机上测。
5.5 场景切换后手柄追踪丢失
现象:重开游戏后手柄位置卡住不动,头显画面正常。
原因:LoadScene 后 XR Origin 重新实例化,但 Input Action Asset 没有重新绑定。
解决:把 XR Origin 做成 Prefab,重开时不要销毁它,只重置水果和分数。或者用 DontDestroyOnLoad 保住 XR Origin,场景里只加载游戏逻辑部分。
6. 把作业变成能讲清楚的作品:源码结构、演示视频与验收技巧
一套能拿得出手的 VR 射击水果作业,不只是 exe 能跑,还要让人看懂你的设计。源码目录建议按功能分文件夹:Scripts 放 C# 脚本,Prefabs 放水果和特效,Scenes 放主场景,Materials 放材质,Audio 放音效。设计文档不用写太长,重点写清楚三个东西:交互流程图、计分规则表、打包环境说明。演示视频控制在 2 分钟内,开头 10 秒展示头显画面和手柄操作,中间 60 秒打水果和计分,最后 20 秒展示重开和打包后的 exe 运行画面。
验收时老师常问的问题就几个:射线怎么发射的、命中怎么判定、分数怎么同步、打包遇到什么问题。你如果能指着 ShootRayController 说清楚TryGetCurrent3DRaycastHit的返回值逻辑,指着 FruitTarget 说清楚isHit锁的作用,基本就稳了。我自己的习惯是每次打包前把 Player Settings 截图存档,演示前 30 分钟在目标机器上完整跑一遍,确认手柄、头显、音效都正常。这个习惯帮我省过至少三次现场翻车。希望帮到你。
本文还有配套的精品资源,点击获取