项目效果演示:
垃圾分类虚拟仿真项目演示
1.环境配置
选择universal 3D(通用渲染管道)项目(不然导入素材包会丢失材质)。
选择Window->Package Manager,安装其中的XR interaction Toolkit。
选择其中的Samples,导入Starter Assets。
选择Edit->Project Settings->XR Plugin Management进行安装。
如果要用电脑模拟器进行VR控制,需要选择Edit->Project Settings->XR interaction Toolkit,勾选 Use XR Devcie Simulator in scenes。
选择Assets\Samples\XR Interaction Toolkit\2.5.4\Starter Assets路径位置下的DemoScene场景,打开运行,观察是否能进行控制。
2.选择界面场景制作(StartScene)
主要功能:射线交互实现主场景(mainScene)和测试场景(TestScene)两个场景的跳转。
新建场景命名为“StartScene”,将DemoScene场景文件拖入Hierarchy面板,把其中的XR interaction Setup拖入到StartScene场景中,点击运行,进行测试。
导入素材包Classification_Resourse.unitypackage,将Assets\Polytope Studio\Lowpoly_Demos\Environment_Free路径位置的Environment_Free场景文件中拖入Hierarchy面板。
把Environment_Free所有元素拖入到StartScene场景中并删除其中的Player游戏对象,把XR interaction Setup作为某个场景元素的子物体,对XR interaction Setup游戏对象的Transform进行reset重置,然后进行手动的坐标调整,就可以在场景中漫游。同时可以对场景元素进行删减与范围缩小,还可以通过设置下图中colliders的位置,限制主体漫游的范围。
导入素材包Classification_Resourse.unitypackage,新建画布Canvas和图像Image和按钮Button(Legacy),选择Assets\Resourse\01图片路径下中3D面板背景图片赋值给image组件,效果大致如下图:
接下来,为UI元素添加组件使其可以进行VR射线交互,为画布添加Tracked Device Graphic Raycaster组件,为按钮添加XR Poke Follow Affordance组件。
编写脚本SkipController.cs,用于控制脚本的跳转。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
//挂载在选择界面上,用于界面的跳转。
public class SkipController : MonoBehaviour
{
// Start is called before the first frame update
//控制返回主界面方法
public void LoadScene(string sceneName)
{
SceneManager.LoadScene(sceneName); // 加载指定名称的场景
}
}
新建空物体,命名为SkipController,将脚本挂载到该物体上,给两个按钮添加点击事件,如下图所示:
提前创建两个场景主场景(mainScene)和测试场景(TestScene),选择File->Build Settings,将三个场景拖入到Scene In Bulid中进行Build,随后测试是否能实现界面的跳转。
3.主界面场景制作(mainScene)
主要功能:通过VR抓取进行与垃圾的交互,需要将垃圾投入到正确的垃圾桶中,通过碰撞体检测和检测类型是否匹配,从而判断否正确分类,进行记录和反馈,也可以跳转到测试界面。
打开mainScene场景文件,导入素材包LowPolyTropicalEnvironment_LITE.unitypackage,将Assets\LowPolyTropicalEnvironment_LITE\Scenes路径位置的TropicalEnvironmentLite_Demo场景文件中拖入Hierarchy面板。
把TropicalEnvironmentLite_Demo所有元素拖入到StartScene场景中并删除其中的摄像头的游戏对象,再把DemoScene中的XR interaction Setup拖入到场景中,把其作为某个场景元素的子物体,进行坐标的调整,效果如下图,进行测试。
接下来进行垃圾分类场景的搭建,将Assets\Resources\02 模型 路径位置下的垃圾桶拖入场景中,调整四个垃圾桶的位置,将Assets\Resources\01 图片 路径位置下的干垃圾、有害垃圾、湿垃圾、可回收的图片拖入场景中,作为垃圾桶的子物体,进行位置的调整。
在Project面板右击,选择create->matertial,调整材料的颜色,并把该材质拖入到Scene中的垃圾桶上(调整整体和轮子的颜色)。
最终效果如下图所示:
为垃圾桶添加碰撞体,选择4个垃圾桶,添加Mesh Collider组件,不勾选Convex选项。
同时需要在底部创建一个碰撞体,用于检测是否分类正确。在Hierarchy面板中,选择垃圾桶右击,选择3D Object->Plane,作为其子物体,编写脚本Trash Can.cs:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//用枚举类型区分垃圾类型
public enum GarbageType
{
recyclable,//可回收
wet,//湿垃圾
dry,//干垃圾
hazardous//有害垃圾
}
//挂载在垃圾桶上
public class TrashCan : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
}
public GarbageType type; // 垃圾桶类型
//垃圾桶底部设置碰撞体,用于碰撞检测,返回垃圾桶本身和垃圾的类型给控制器进行逻辑判断。
private void OnCollisionEnter(Collision collision)
{
Garbage garbage = collision.collider.GetComponent<Garbage>();
if (garbage != null)
{
ClassificationManager.Instance.OnGarbageCollision(garbage.type, type);
Debug.Log("当前垃圾桶的类型为:" + type + " 当前垃圾桶的类型为: " + garbage.type);
if (type != garbage.type)
{
garbage.ResetToStartPosition();
}
}
}
}
然后挂载在该物体上,同时Plane需要进行调整位置、颜色,设置对应的类型。
将Assets\Resources\02 模型 路径位置下的“桌子”模型拖入场景中,调整到合适的大小和为止,为其添加Box Collider组件,点击Edit Collider,调整其碰撞体大小,同时添加Rigidbody组件,勾选Is Kinematic选项,使其不会受碰撞影响。
将Assets\Resources\02 模型 路径位置下的“大猩猩手办”、“可乐罐”、“药品2”、“蛋糕”、“陶瓷杯”、“电池”的模型拖入场景中,部分模型需要手动拖动material进行填色。
随后选中所有物体,为其添加Box Collider、Rigidbody、XR Grab Interactable组件,在Box Collider组件中点击Edit Collider,单独调整其碰撞体大小,并将物体放置在桌面上。
新建脚本Garbage.cs,并挂载在物体上。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//挂载在垃圾类上
public class Garbage : MonoBehaviour
{
public GarbageType type; // 垃圾类型
private Vector3 originalPosition; // 原始位置
private Rigidbody rb;
private void Start()
{
rb = GetComponent<Rigidbody>();
originalPosition = transform.position;
}
//当被错误分类时调用,用于把垃圾重新放回桌子上
public void ResetToStartPosition()
{
transform.position = originalPosition;
rb.velocity = Vector3.zero;
rb.angularVelocity = Vector3.zero;
}
}
在Garbage(Script)组件中设置每个物体的类型Type,类型如下表:
模型 | 实际垃圾类型 | 设置的Type |
大猩猩手办 | 可回收 | Recycle |
可乐罐 | 可回收 | Recycle |
药品 | 有害 | Hazardous |
蛋糕 | 湿垃圾 | Wet |
陶瓷杯 | 干垃圾 | Dry |
电池 | 有害 | Hazardous |
创建脚本ClassificationManager.cs,用于垃圾分类逻辑处理。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
using UnityEngine.Events;
//挂载在主界面(垃圾分类)上的控制器
public class ClassificationManager : MonoBehaviour
{
public static ClassificationManager Instance; // 单例模式
public GameObject correctUI;//正确UI
public GameObject incorrectUI;//错误UI
public AudioSource audiosource;//播放器
public AudioClip correctSound;//正确音效
public AudioClip incorrectSound;//错误音效
public GameObject CountUI;//用于统计正确分类的个数
private int Count=0;//个数
private void Start()
{
correctUI.SetActive(false);
incorrectUI.SetActive(false);
CountUI.SetActive(true);
CountUI.GetComponent<Text>().text = "你目前正确分类的物体个数为:" + Count;
}
//用于判断垃圾和垃圾桶是否是同一类别的
public void OnGarbageCollision(GarbageType garbageType, GarbageType trashCanType)
{
if (garbageType == trashCanType)
{
CorrectDispose();
Count += 1;
}
else
{
IncorrectDispose();
}
CountUI.GetComponent<Text>().text = "你目前正确分类的物体个数为:" + Count;
Debug.Log("Your score is: " + Count);
}
private void CorrectDispose()
{
audiosource.clip = correctSound;
audiosource.Play();
incorrectUI.SetActive(false);
//控制UI先显示,2秒后消失
correctUI.SetActive(true);
Invoke("DisappearObject", 2f);
}
private void IncorrectDispose()
{
audiosource.clip = incorrectSound;
audiosource.Play();
correctUI.SetActive(false);
incorrectUI.SetActive(true);
Invoke("DisappearObject", 2f);
}
//挂载在按钮上,界面跳转
public void LoadScene(string sceneName)
{
SceneManager.LoadScene(sceneName); // 加载指定名称的场景
}
private void DisappearObject()
{
correctUI.SetActive(false);
incorrectUI.SetActive(false);
}
}
在场景中创建一个空物体,命名为ClassificationManager,把创建的脚本挂载上去,为其添加Audio Source组件,对脚本中的Audiosource进行赋值,同时将Assets\Resources\03 音效 路径位置下的“正确音效”、“错误”文件拖入到Correct Sound和Incorrect Sound中进行赋值,效果如下图所示:
接下来,需要制作UI界面,需要在场景中新建画布Canvas、用于显示已分类数量的Text(Legacy)和用于跳转界面的按钮Button(Legacy)。同时新建3个Image游戏对象,用于显示背景、正确图像和错误图像,将Assets\Resources\01 图片 路径位置下“回顾3”、“正确”、“错误”的图片赋值给Image,修改UI元素名称,便于区分,调整UI元素位置,最终效果如下图所示:
为了使其能进行VR射线交互,给画布添加Tracked Device Graphic Raycaster组件,为按钮添加XR Poke Follow Affordance组件。
接下来继续给ClassificationManager物体进行参数的赋值。
进行测试,是否能进行判断垃圾分类的正误并进行反馈。
4.测试场景制作(TestScene)
主要功能:能通过射线交互,能进行选择题选项的选择和判断题的对错的选择。
首先导入素材,打开TestScene场景文件,将DemoScene场景文件拖入Hierarchy面板,把其中的XR interaction Setup拖入到StartScene场景中,将Assets\Resources\02 模型 路径位置下的房屋模型导入到场景中,调整位置,让XR interaction Setup处于房屋之中,效果大致如下图,进行测试。
接下来要进行答题界面的制作,我们可以打开将Assets\Resources\06 考题 路径位置下的垃圾分类考题文件,进行题干和选项的内容的设置。
首先新建一个画布Canvas,调整位置,为其添加TrackedDeviceGraphicRaycaster组件。新建Image子物体,设置画布的背景图片。
然后题目中有单选题和判断题,需要进行区分。创建脚本Question.cs,用于区别题型和设置答案:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
//挂载在具体题目上(判断题、选择题)
public class Question: MonoBehaviour
{
public Toggle[] options; // 单选题的选项
public int correctAnswerIndex; // 单选题正确答案的索引
public Toggle trueFalseToggle; // 判断题的Toggle
public bool correctAnswer; // 判断题的正确答案
public bool isChoice; // 判断是选择题还是判断题
}
接下进行题目的UI制作。
选择题的制作(以第一题为例):
创建一个空物体作为新建画布的子物体,命名为Question1。为其挂载Question脚本,并设置属性如下图所示(4个选项、D选项为正确选项,为选择题),点击Options下方“+”号,设置数量为4,设置Correct Answer Index的值为3,勾选 Is Choice选项。
同时为Question1游戏对象添加Toggle Group组件,用于控制其子对象的Toggle只能被选中一个,用于模拟单选题。同时勾选Allow Switch Off 选项,允许程序运行时,可以没有默认的勾选选项。
在Question1下新建Text(Lecary)和4个Toggle,作为其子物体,调整位置、颜色,并对选项进行取名(1_A,1_B,1_C,1_D),便于与后面的选择题选项区分。同时需要为每一个Toggle游戏对象添加XR Poke Follow Affordance组件。
选项新建完成后,对Question1的Question组件中的Option进行赋值,如下图所示:
这样一道选择题的设置就完成了。
判断题的制作(以第二题为例):
创建一个Toggle作为画布Panel的子物体,命名为Question2,为其挂载XR Poke Follow Affordance组件,同时挂载Question脚本,并设置属性如下图所示(答案为正确,为判断题),勾选Correct Answer选项,不勾选Is Choice。
新建一个Text(Legacy)作为Question2游戏对象的子物体,编辑其内容,显示题干,如下图所示:
这样一道判断题就编辑好了,按如下的两种方式将5道题目编辑好后(注意命名要进行区分),我们还需要提交按钮和返回主界面的按钮Button和显示分数的UI。在画布中新建两个按钮Button(Legacy),分别命名为returnButton、SubmitButton,新建Text(Legacy),命名为Score,在Hierarchy面板中将returnButton拖到到Score下,作为其子物体。
调整其位置,最后效果如下图所示:
到此为止,我们UI界面制作完毕。
接下来我们需要新建脚本TestController.cs,用于实现判断对错、交互的功能:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
//挂载在测试界面的控制器上。
public class TestController : MonoBehaviour
{
public List<Question> questions; // 包含所有问题的列表
public int score = 0; // 最终得分
public GameObject ScoreUI;
private void Start()
{
ScoreUI.SetActive(false);
}
// 用户点击提交按钮时调用的方法
public void SubmitAnswers()
{
score = 0;
foreach (Question question in questions)
{ //如果是选择题
if (question.isChoice)
{
// 单选题
if (question.options[question.correctAnswerIndex].isOn)
{
score++;
}
}
else
{
// 判断题
if (question.trueFalseToggle.isOn== question.correctAnswer)
{
score++;
}
}
}
ScoreUI.SetActive(true);
ScoreUI.GetComponent<Text>().text = "你的最终得分是:" + (score*1.0/questions.Count)*100;
Debug.Log("Your score is: " + score);
// 可以在这里添加代码显示得分或者转到下一个场景
}
//控制返回主界面方法
public void LoadScene(string sceneName)
{
SceneManager.LoadScene(sceneName); // 加载指定名称的场景
}
}
新建空物体,命名为TestController,挂载上该脚本,对Questions和Score UI属性进行赋值,如下图所示:
为两个按钮分别添加跳转场景的功能和提交答案的功能,添加点击事件,如下图所示:
最后进行测试,功能是否正常。