请在前几讲的基础上进行
一、准备工作
1.在Assets>Scenes
中新建场景,重命名为HandTrackingLearning
,并打开。
2、打开Assets>NRSDK>Models>Hands>RightHand
3、将其另存为Paper.prefab
、Rock.prefab
和Scissors.prefab
4、打开Scissors
,注意左侧的Hierarchy(层级)
栏,下面解释一下各部分的含义(个人理解,并未找到官方说明,建议亲自实验):
- wrist 手腕
- middle_1 中指从下数第1个关节
- middle_2 中指从下数第2个关节
- middle_3 中指第3个关节
- middle_end 中指指尖(作用不清楚)
接下来的各部分只是对应的指头不同,但含义相同 - pointer 食指
- ring 无名指
- thumb 大拇指
- pinky 小拇指
注意:pinky上方还有一级,名为:wristpadding
,指的是小拇指下方、掌纹处的关节;同样,thumb_1
实际上也不是通常意义上的大拇指第一节,而是大拇指的掌处关节。
5、将Scissors
的手型调整为游戏“石头剪刀布”中“剪刀”的手型。
为了节约大家时间,大家可以通过本链接( 提取码:fs51)下载我调整好的手型,不过是在是有些难看,大家可以自行调整一下。
6、将Rock
调整为“拳头”的手型。
二、正式开发
1、打开先前创建的HandTrackingLearning
,删除场景中的Main Camera
,新建NRCameraRig
、NRInput
和Canvas
(注:前两个部件均位于Assets>NRSDK>Prefabs
,拖入Hierarchy(层级)
栏即可;新建Canvas
的步骤为:在Hierarchy(层级)
栏空白处右击,选择UI>Canvas(画布)
。详见下面的若干图片。)。
删除和创建完成后,Hierarchy(层级)
栏应如下图所示:
2、点开Canvas
,将其Render Mode(渲染模式)
改为World Space(世界坐标)
,然后重置其坐标。
4、打开NRInput
,将Input Source Type
改为Hands
。
5、在游戏中,我们需要一个按钮来开始,所以在Canvas
下面先创建一个空对象,命名为StartButton
,再在空对象下创建一个按钮,方法如图:
创建好后如下图:
打开Text
,修改文本和字体大小。(下图为推荐数据)
调整开始按钮的位置,使得能够在相机视角中看到它。(在我们完成整个项目后,认为将位置Z设为10,缩放设为0.05,0.05,1
更为合适,所以请忽视下图的数据)。
6、与上述同理,在Canvas
下再创建4个空对象,分别命名为Start
、Win
、Lose
、Tie
,将其位置改为x=0,y=30,z=350
。四个对象下分别创建Text,内容改为Ready...3,2,1,Go!
、You Win!
、You Lost!
、You’re Tied!
,字体大小改为20
,颜色改为白色(数据及颜色均为推荐数据和推荐颜色)。创建后如图所示:
7、分别打开Start
、Win
、Lose
、Tie
四个对象,在检查器
一栏中取消勾选。效果如图所示。(注:在我们进行完全部开发后,我们认为将Start
也取消勾选更为合适,但由于后面的教程图片已经完成,所以请您主动忽视跟此相关的问题)
编写脚本
1、在Assets
下新建一个文件夹Scripts
,并打开,新建C#脚本
,重命名为MenuManager
,用来控制这些提示语的出现。
打开该脚本,将以下代码编写到脚本中。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MenuManager : MonoBehaviour
{
[SerializeField] GameObject[] menus;
public static MenuManager Instance;
private void Awake()
{
Instance = this;
}
public void OpenMenu(string menuName)
{
foreach(var item in menus)
{
item.SetActive(false);
}
foreach(var item in menus )
{
if(item.name==menuName)
{
item.SetActive(true);
break;
}
}
}
}
2、创建一个名为RPSHand
的脚本,用来写判断类。编写以下代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using NRKernal;
public class RPSHand : MonoBehaviour
{
public string name;
public virtual string Judge(string _name)
{
return null;
}
}
3、创建一个名为RPSManager
的脚本,用来编写我们需要的游戏逻辑。编写以下代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using NRKernal;
using UnityEngine.UI;
using System.Globalization;
public class RPSManager : MonoBehaviour
{
[SerializeField] GameObject[] hands;
[SerializeField] Text gestureTxt;
private bool isPlaying;
private GameObject currentHand;
public void OnClick()
{
if (isPlaying) return;
isPlaying = true;
foreach(var item in hands)
{
item.SetActive(false);
}
MenuManager.Instance.OpenMenu("Start");
StartCoroutine("DelayHand");
}
IEnumerator DelayHand()
{
yield return new WaitForSeconds(3);
currentHand = hands[Random.Range(0, hands.Length)];
currentHand.SetActive(true);
if(currentHand.GetComponent<RPSHand>().Judge(gestureTxt.text)=="win")
{
MenuManager.Instance.OpenMenu("Win");
}
else if(currentHand.GetComponent<RPSHand>().Judge(gestureTxt.text) == "lose")
{
MenuManager.Instance.OpenMenu("Lose");
}
else
{
MenuManager.Instance.OpenMenu("Tie");
}
isPlaying= false;
}
}
4、接下来是三种手势的判断逻辑,同样是在Scripts
的文件夹下,分别创建三个脚本,RockHand
,PaperHand
和ScissorHand
。三个脚本分别如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class RockHand : RPSHand
{
public override string Judge(string _name)
{
if (_name == "R:Paper")
{
return "win";
}
else if (_name == "R:Rock")
{
return "tie";
}
else return "lose";
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PaperHand : RPSHand
{
public override string Judge(string _name)
{
if (_name == "R:Paper")
{
return "tie";
}
else if (_name == "R:Rock")
{
return "lose";
}
else return "win";
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ScissorHand : RPSHand
{
public override string Judge(string _name)
{
if (_name == "R:Paper")
{
return "lose";
}
else if (_name == "R:Rock")
{
return "win";
}
else return "tie";
}
}
四、脚本使用
1、在Hierarchy(层级)
栏中点开Canvas
,将脚本MenuManager
拖入,点击Menus
,点击4次加号,将Hierarchy(层级)
栏中Start
、Win
、Lose
和Tie
依次拖入Menus
的四个元素。点击Add Component(添加组件)
,搜索组件Canvas Raycast Target
并添加。
2、新建一个空对象并命名为GameManager
,将先前创建的三个手势模型拖入GameManager
下作为子对象,然后调整三个子对象的模型位置直至出现在合适的视角范围内。(注:我们建议将三个手掌的位置全部设为(x=0,y=0,z=0)
,这是一个更好的位置。由于这是我们在完成整个项目开发之后发现的,所以后面的图片全部都存在与此相关的问题,请您忽视)。
3、分别打开上述三个模型子对象,分别添加名为Animator
的组件,再添加对应的控制脚本文件,最后取消勾选最上方的选项。
4、将RPSManager
关联到GameManager
这个对象上,并将三个手势模型添加到Hands
下面。
5、在资产中搜索NRHand
,将NRHand_L
和NRHand_R
分别拖入NRInput
中作为其中Right
和Left
的子对象。
五、手势识别
1、在资产中,搜索HandTracking
,将名为HandTracking
的场景拖入层级
栏中。将HandTracking
场景的NRInput>Right(或Left)>NRHand_R(NRHand_L)>GestureSimpleTip_R(GestureSimpleTip_L)
复制,粘贴为HandTrackingLearning
场景中NRInput>Right(或Left)>NRHand_R(NRHand_L)
的子对象。
2、在层级
栏中移除整个HandTracking
场景。
3、在Assets>Scripts
中新建脚本GestureRPSTip
,内容如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using NRKernal;
using NRKernal.NRExamples;
using System.Runtime;
public class GestureRPSTip : GestureSimpleTip
{
public class GestureRPSName
{
public const string Gesture_Point = "Point";
public const string Gesture_Rock = "Rock";
public const string Gesture_Scissor = "Scissors";
public const string Gesture_Paper = "Paper";
}
public override void UpdateGestureTip()
{
var handState = NRInput.Hands.GetHandState(handEnum);
if (handState == null)
return;
switch (handState.currentGesture)
{
case HandGesture.Point:
gestureTxt.text = string.Empty;
break;
case HandGesture.Grab:
gestureTxt.text = GetHandEnumLabel() + GestureRPSName.Gesture_Rock;
break;
case HandGesture.Victory:
gestureTxt.text = GetHandEnumLabel() + GestureRPSName.Gesture_Scissor;
break;
case HandGesture.OpenHand:
gestureTxt.text = GetHandEnumLabel() + GestureRPSName.Gesture_Paper;
break;
default:
gestureTxt.text = string.Empty;
break;
}
if (handState.isTracked)
{
Pose palmPose;
if (handState.jointsPoseDict.TryGetValue(HandJointID.Palm, out palmPose))
{
UpdateAnchorTransform(palmPose.position);
}
tipAnchor.gameObject.SetActive(!string.IsNullOrEmpty(gestureTxt.text));
}
else
{
tipAnchor.gameObject.SetActive(false);
}
}
}
4、在资产中寻找GestureSimpleTip
脚本并打开。
将代码第33
行private
改为virtual public
,81
、95
行private
改为virtual public
。
5、打开两个GestureSimpleTip
对象,将原有的Gesture Simple Tip
组件移除,再将新创建好的GestureRPSTip
文件关联上,调整Hand Fnum
(左手选Left Hand
,右手选Right Hand
),再将层级
栏中的NRInput>Right(Left)>NRHand_R(NRHand_L)>GestureSimpleTip_R(GestureSimpleTip_L)>TipAnchor
和NRInput>Right(Left)>NRHand_R(NRHand_L)>GestureSimpleTip_R(GestureSimpleTip_L)>TipAnchor>Canvas>PanelBg>GestureTxt
两个对象拖入Tip Anchor
和Gesture Txt
。
6、在Assets
下新建文件夹Controller
,进入后新建三个动画控制器
,命名为Paper
、Rock
、Scissor
。
7、将三个动画控制器
拖入相应模型的Animator
的控制器
中。
8、打开Canvas>StartButton>Button
,找到鼠标点击
,点击+
,将GameManager
拖入,最后选择RPSManager.OnClick
。
六、导出文件
像以往一样将文件导出,在Nreal Light中运行即可。
七、后记
由于我们教程的制作也是在学习他人教程的同时进行的,在本教程的制作过程中,我们参考的教程出现多处操作没有进行说明,导致我们最后的项目一度处于停滞不前的状态。在经过多日的研究、查阅资料后,我们才总算解决所有问题,利用markdown写下了这篇9100余字的教程。由于过程中我们也有部分步骤由于没有经验而出错,所以图片上会存在问题。如有任何需要帮助的地方,欢迎留言。