1,素材
在unity的asset store上下载一个免费的素材“Low-Poly Table Tennis Set”,先“Add to My Assets”,然后在Unity中下载,Window -> Package Manager,如下图,并import:
打开它的场景,在场景中新建XR Rig(见上一章)。下图是目前项目里的物体(不是最初的,增删了一些):
2,转动Snap Turn
通过手柄的遥感来调整朝向
点击XR Rig对象,在Inspector面板中点击“Add Component”,为XR Rig添加“Snap Turn Provider”组件:
设置Turn Amount,代表推一次遥感转多少度。在Controllers变量中“+”一个元素,用右手柄来赋值,直接将XR Rig下面的“RightHandController”拖到Element0这边即可。
这就完成了转动,在场景中,通过遥控右手柄的遥感来控制自己的转动朝向,每次转动30度。
extra知识点:
Unity中的UI事件都会有回调函数,可以用AddListener或在inspectior面板中来添加相关事件的回调。但目前没发现pico手柄上按键的回调,都是通过在Update中用controller.inputDevice.TryGetFeatureValue函数来获得按钮的值。直接这样写会有问题,比如按下X按钮,会在多个Update帧中都触发,而通常情况下我们只想一次按钮触发一次。SnapTurnProvider这里面的解法是设定了一个时间间隔:
private void Update() { // wait for a certain amount of time before allowing another turn. if (m_TimeStarted > 0.0f && (m_TimeStarted + m_DebounceTime < Time.time)) { m_TimeStarted = 0.0f; return; } 。。。
3,传送Teleportation
通过点击地面将自己传送过去(用常规的方法走过去会产生眩晕,传送是VR中的一个解决方案)
首先给XR Rig添加“Teleportation Provider”和“Locomotion System”组件,并赋值XR Rig,如下:
再给地面Floor对象添加“Teleportation Area”组件并赋值:
4,抓取物体
设置左手柄通过射线抓物体,右手柄近距离抓取物体(更符合实际)。
左手控制器LeftHand Controller上添加XR Ray Interactor即可
右手控制器不用射线,可以删除Line Render和XRInteractor line Visual组件。增加XR Direct Interactor和Sphere Collider。Direct Interactor通过Collider来跟被抓物体交互,所以Collider的半径就是跟物体的接触距离:
被抓物体添加XR Grab Interactable组件,这边给球拍和球添加,就可以用手抓拍,左手拿球(按手柄的grip按钮)。
可以把球抛在空中,用球拍去打,球需要有Rigidbody重力,我把球拍的Rigidbody重力去掉了。但这时弹力不够
5,添加弹力
新建Physical Material,设置弹力Bounciness
把Physical Material拖拽赋值给球、球拍、球桌、地板、墙面的Collider的Material变量,这样就有了弹力
我这边设置了多个不同参数的Physical Material,赋值给不同的物体,可自行去调整,符合自己的感觉。
这时可以自己发球了,但是球打出去到处乱跑。想做三个控制:
- 自己发球:点击按钮,球重置到球台上,便于抓取;
- 对方发球:点击按钮,让球从对面发过来,就可以接球一次了;
- AI:对面的球拍可以接球,跟我对打起来(哪怕一个回合也行);
前两个通过unity的UI来做
6,UI
UI还是Unity的这一套东西,只不过可以通过手柄的射线去交互。新建Canvas画布,添加Tracked Device Graphic Raycaster组件;在画布下新建Panel,在Panel下新建两个按钮:
我把UI放在对面的墙上,方便操作
两个球拍一个作为自己的球拍,一个作为对手AI的球拍,重命名如上面的Hierachy图。新建脚本BatPlayer_AI.cs(只贴主要代码,代码组件中有些物体变量需要在Inspector中去设置,从这边开始就不单独展示了,否则有点繁琐),在这里添加AI的一些脚本,同时把这两个按钮的回调函数也写在这这脚本中先:
public void ServeBall_AI()
{
ball.transform.position = startTransform_AI.position + new Vector3(Random.Range(-0.5f, 0.5f), 0, 0);
ball.transform.rotation = startTransform_AI.rotation;
ball.velocity = new Vector3(0, -force * 0.3f, -force);
transform.position = startTransform_AI.position + new Vector3(0, 0, 0.4f);
transform.rotation = Quaternion.Euler(90, 0, 0);
isCatchBall = false;
}
public void ServeBall_FPS()
{
ball.transform.position = startTransform_FPS.position;
ball.velocity = Vector3.zero;
}
然后通过AddListener或直接在Inspector中手动将上面的函数绑定到按钮的OnClick事件上(这边是手动绑定,但用代码添加的方式更好,因为手动绑定不利于管理,而且复制到另一个项目或场景中容易丢失绑定关系)
这边需要去调整代码中发球的参数,保证球正确的发过来,我这边的XR Rig朝向是X轴。现在就可以自己发球了,球跑了可以重置,练习发球;也可以让对方发球练习接球。接下来希望对方也能接住我的球。
7,AI
方法1:
当球与对面的球桌碰撞后,AI开始工作,选择合适的时机(当球的y方向速度足够小),沿着球运动的反方向(只看X和Z轴,Y轴速度此时也接近0了,先不管)击球,只要控制好力度(还没调好。。),就可以将球打回来,大致代码如下:
void Update()
{
if(isCatchBall && (ball.velocity.z > 0) && (ball.transform.position.y > minCatchHeight))
{
CatchBall_AI();
}
}
void CatchBall_AI()
{
if (ball.velocity.y > 0.05f)
{
transform.rotation = Quaternion.LookRotation(new Vector3(ball.velocity.z, 0, -ball.velocity.x)) * Quaternion.Euler(90, 0, 0);
transform.position = ball.transform.position + new Vector3(0, 0, catchB + catchW * ball.transform.position.x);
//GetComponent<Rigidbody>().MovePosition(ball.transform.position + new Vector3(0.1f, 0, 0));
}
else
{
GetComponent<Rigidbody>().MovePosition(ball.transform.position);
isCatchBall = false;
}
}
isCatchBall变量会在球与球桌碰撞后触发的函数中判断,新建脚本BallController.cs绑在球上,添加碰撞回调函数,这里的enableCatchBall()就是设置isCatchBall变量。
void OnCollisionEnter(Collision collision)
{
if (collision.gameObject.Equals(table) && (transform.position.z > 0.15f) && (GetComponent<Rigidbody>().velocity.z > 0))
{
batPlayerAI.logText.text = "OnCollisionEnter" + transform.position;
batPlayerAI.enableCatchBall();
}
else
{
batPlayerAI.disableCatchBall();
}
}
这里加了一系列的hard code来判断各种触发条件以及调整击球的各种参数,不优雅,不过这只是一个温习untiy的demo。
方法2:
有时间学习一下unity中的强化学习模块ML-Agents,先列个TODO。
8,其他细节
上面记录了主要的一些步骤,细节调的比较多,比较繁琐,没法列全
a,球会穿模甚至飞出屋子
因为球速太快,碰撞检测时已经穿过碰撞体。一个方法是增加墙面地面的collider的厚度,但只能缓解部分问题,这边需要调整球的Rigidbody的Collection Detection方式(其他物体的Rigidbody也调一调):
b,XR Rig的视野需要设置一下,不然太近的物体看不到,XR Rig/Camera Offset/Main Camera:
c,一直按着右手柄的grip键来握拍,感觉有点伤手柄,可以写一个脚本,让球拍跟着手柄移动,去掉右手柄的XR Direct Interactor组件,写一个脚本BatPlayer1.cs绑在自己的球拍上,在FixedUpdate中实时更新球拍的位置,这边不能直接设置transform的position和rotation,因为这样物理属性就失效了,要用Rigidbody的相关move函数来让刚体运动,这样手挥拍的时候才能击打球:
private void FixedUpdate()
{
GetComponent<Rigidbody>().MovePosition(rightHand.position);
GetComponent<Rigidbody>().MoveRotation(rightHand.rotation * Quaternion.Euler(0, -90, 0));
}
d,CatchBall_AI()和上面(c)的代码中,多了一个乘以一个四元数Quaternion的操作,是因为场景中球拍的XYZ朝向跟实际需要的朝向不一致,所以多一步旋转的操作。