第3回では、Unity XR Interaction Toolkit
Unity XRTKのInteractorを知る
Unity XRTKはVRの開発をスムーズに進めるためのUnity公式パッケージで、本連載の第2回ではUnity XRTKに搭載された各種スクリプトのパラメータを調整することで、VRの移動方法の仕様が変えられることを解説しました。今回はUnity XRTKの要ともいえるInteractor、すなわち
プレイヤーがオブジェクトへ干渉する方法は、Unity XRTK上に用意されたComplete XR Origin Set Up -> XR Origin (XR Rig) -> Camera Offset -> Right ControllerとLeft Controller の中にある
これらの操作を実現するためには、干渉されるオブジェクト側に
また、Interactorには
近くのオブジェクトを直接触る/つかむ:Direct Interactor
Direct Interactor
なお、好みの問題ですが
遠くのオブジェクトを触る/つかむ:Ray Interactor
Ray Interactorはコントローラ
Ray Interactorは、VRだとおもにプレイヤーの移動にかかる労力を省略する目的で、以下2つの用途に使います。
遠くのオブジェクトをつかむ
VRコンテンツにおいては
少し困ったことに、プレイヤーがオブジェクトをつかんだときの処理がRay InteractorとDirect Interactorでやや異なります。RayとDirectのどちらも
UIパネルを操作する
VRにおけるメニュー画面は、プレイヤーから見て1~2メートルほどの大きさのパネルをレーザーポインターで操作することが多いです。これをUnityで実装するときはRay Interactorを用います。
オブジェクトをつっつく:Poke Interactor
Pokeは日本語で
VRゲームにおいて
もちろんインタラクションの楽しさや手ごたえを理由に指で押せるボタンが実装されているVRゲームも多数ありますが、それらはPoke Interactorではなく、物理演算によって制御されることが多いです。ジャンルにもよりますが、直観的には人間の指先だけでなくプレイヤーが手に持った剣やハンマーといった剛体のオブジェクトでもボタンは押せてしかるべきであり、
オブジェクト側の制御:XR Grab Interactable
次はプレイヤーに干渉されるアイテム側の制御について説明します。XR Grab InteractableはVRで欠かせない
まず左のHierarchyウィンドウを右クリックし、3D Objectから
まずは
Selectはプレイヤーがオブジェクトを手に持ったときです。プレイヤーの手がオブジェクトにHoverしている状態でMeta Quest Controllerの中指のグラブボタンを押下すると、手にオブジェクトを持つことができます。
Activateはプレイヤーが手に持っているオブジェクトを使ったときです。プレイヤーがオブジェクトをSelectして手に持っている状態で、Meta Quest Controllerの人差し指のトリガーを押下すると、手に持っているオブジェクトを使うことができます
これらを前提に、XR Grab InteractableのInteractable Events
イベント名 | 説明 |
---|---|
First Hover Entered | Hoverに手が初めて入ったとき |
Last Hover Exited | Hoverから手が最後に抜けたとき |
Hover Entered | Hoverに手が入ったとき |
Hover Exited | Hoverから手が抜けたとき |
First Select Entered | 手が初めてSelectしたとき |
Last Select Exited | 手が最後にSelectをやめたとき |
Select Entered | 任意の手がSelectに入ったとき |
Select Exited | 任意の手がSelectから抜けたとき |
Activated | 使われ始めたとき |
Deactivated | 使われ終わったとき |
さて、一部にある
仮に
逆に
なお、先述のRay Interactorで触れた
「放置されたオブジェクトが元の場所に戻る」仕様を実装する
ここまでプレイヤーがVRで道具に触れたり使ったりする機能について説明しました。一方で、VRにおいてプレイヤーが道具を持ち運ぶためには、プレイヤーのVR上の身体のどこかに道具を
そこで、まずは前段として
// 時間経過で道具の位置を元に戻すスクリプトの例
using UnityEngine;
public class ItemCountDown : MonoBehaviour
{
//// ゲームオブジェクトのRigidbodyを取得する
//private Rigidbody m_rigidbody;
// ゲームオブジェクトの復帰する座標(を持つゲームオブジェクト)を指定する
[SerializeField] public GameObject m_gameObjectRestore;
// 復活する座標を保持するための変数
private Transform m_transtormRestore;
// ゲームオブジェクトがすでに動いたかどうか
private bool m_isMoved = false;
// プレイヤーがゲームオブジェクトに触れているかどうか
private bool m_isTouching = false;
// ゲームオブジェクトの位置がリセットされるまでの時間制限
public float m_grabItemTimeLimit = 3.0f;
// スクリプト内のタイマーに用いる変数
private float m_timer;
// Start is called before the first frame update
void Start()
{
m_timer = 0.0f;
}
// プレイヤーに握られたとき
public void GetGrab()
{
m_isMoved = true;
m_isTouching = true;
}
// プレイヤーに離されたとき
public void ExitGrab()
{
m_isTouching = false;
}
// Updateは毎フレーム実行される
void Update()
{
// 制限時間が0秒の場合は、位置のリセットを実行しない
if (m_grabItemTimeLimit != 0)
{
// ゲームオブジェクトが動いたかどうか
if(m_isMoved == true)
{
// プレイヤーが現在アイテムに触っているかどうか
if(m_isTouching == false)
{
// カウントダウンを進める
m_timer += Time.deltaTime;
//カウントダウンが制限時間を迎えたらゲームオブジェクトの位置をリセット
if(m_timer > m_grabItemTimeLimit)
{
// ゲームオブジェクトの速度をリセットする
var rigidbody = GetComponent<Rigidbody>();
rigidbody.velocity = Vector3.zero;
// ゲームオブジェクトを指定箇所に配置する
rigidbody.transform.position = m_gameObjectRestore.transform.position;
rigidbody.transform.rotation = m_gameObjectRestore.transform.rotation;
// ゲームオブジェクトは不動の状態に戻る
m_isMoved = false;
// カウントダウンをリセットする
m_timer = 0.0f;
}
}
// プレイヤーが触っている場合はカウントダウンをリセット
else
{
m_timer = 0.0f;
}
}
}
}
}
上記のスクリプトは
VRで道具を持ち運ぶための強力な機能「Socket Interactor」
Unity XRTKではプレイヤーが道具を持ち運ぶ操作を実現するときに
ただ、意図しないオブジェクトがソケットに収納されてしまうのは困ります。このためUnity XRTKにはInteraction Layer Maskという機能が用意されており、たとえばXR Grab Interactableに
(1)ホルスター型
肩と腰といった身体の部位にソケットを配置する手法です。プレイヤーは、自らの肩や腰に手をやることで、そこに固定したアイテムを取り出すことができるようになります。たとえばシューター系のVRゲームであれば、両肩に大型の銃器、両腰にピストルなど小型の銃が配置されていることが多いです。現実でもショットガンは背中に背負って担ぐことが多いため、肩から取り出す実装になります。ピストルなど小型銃は腰元のホルスターに吊り下げておくのは想像しやすいでしょう。
ホルスター型のデメリットとしては、特に肩が
Unityで実装する際は、プレイヤーの頭の真下にソケットを配置しつつ、プレイヤーの頭の直下に垂直な上半身がぶらさがっているとみなして、頭の垂直下の座標に合わせて両肩と両腰にUnity Socketを配置するのがシンプルな方法です
以下は、図10で実装しているスクリプト、Waist Holsters Lerpの実装例です。ソケットをプレイヤーのCameraOffset直下に配置しつつ、PlayerCameraのY軸の角度に合わせて回転しつづける処理を行っています。
// HolsterLerpの実装例
using UnityEngine;
public class WaistHolstersLerp : MonoBehaviour
{
// XR Origin (XR Rig)直下のCameraOffsetを
[SerializeField] Transform _cameraOffset;
// ソケットの角度をカメラの向きに合わせる
[SerializeField] Transform _playerHead;
[SerializeField] private float timeRatio = 0.5f;
private float timeCount;
// Start is called before the first frame update
void Start()
{
// タイマーを初期化
timeCount = 0.0f;
}
// Update is called once per frame
void Update()
{
// 回転の処理にはクォータニオンを使う
transform.rotation = Quaternion.Slerp(_cameraOffset.rotation, _playerHead.rotation, timeCount * timeRatio);
// rotationをY軸以外0にする処理
transform.rotation = Quaternion.Euler(new Vector3(0f, transform.eulerAngles.y, 0f));
timeCount = timeCount + Time.deltaTime;
}
}
また、ホルスターだけでなくベストの形状になるようにソケットを配置することも可能です。特にミリタリー要素の強いVRゲームはプレイヤーが持ち運ぶオブジェクトの量が多いため
(2)手首型
『Half-Life: Alyx』
ただしオブジェクトをソケットから取り出すときにスケールをもとに戻す処理まではしてくれない
スケールをもとに戻す処理はいたってシンプルです。ゲームの起動時のスケールを保持しておいて、プレイヤーがソケットから取り出したタイミングで最初に保持したスケールを適用すれば問題ありません。オブジェクトのHover Exited
using UnityEngine;
public class ItemScaleRecover : MonoBehaviour
{
// ゲームオブジェクトのスケールを保持する変数
private Vector3 m_defalutScale;
// Start is called before the first frame update
private void Awake()
{
// シーン起動時のゲームオブジェクトのスケールを保持する
m_defalutScale = transform.localScale;
}
public void RecoverScale()
{
// 小型ソケットから取り出したときにゲームオブジェクトのスケールを元に戻す
transform.localScale = m_defalutScale;
}
}
(3)バックパック型
VRでは現実と同じようにインベントリーがバックパックの形状をしていることが多く、プレイヤーの背中からアイテムを取り出すことがしばしばあります。この実装は、バックパックの中にソケットがいくつか用意されている場合
後者の親子紐づけ型のバックパック、実はものすごくシンプルな実装です。バックパック内の空間にボックス状のコリジョンを配置し、バックパックに放り込まれた任意のオブジェクトがボックスのコリジョンにぶつかったら、そのオブジェクトをバックパックの子オブジェクトとみなして物理演算を止めるだけ。今回はオブジェクトがぶつかったコリジョンがバックパックかどうかを判別するのにタグを使っていますが、タグ以外の実装をしたい場合はコリジョンのレイヤーやInteraction Layerなど活用して改変してみてください。
このオブジェクト親子紐づけ型のインベントリーはあまり数多くのVRゲームで採用されているわけではありませんが、SteamVRやQuest StoreのベストセラーVRシューター
using UnityEngine;
public class ItemAttachParent : MonoBehaviour
{
// ゲームオブジェクトがぶつかったとき
public void OnTriggerEnter(Collider other)
{
// ぶつかったゲームオブジェクトのコリジョンのレイヤーの種類を文字列にする
string layerName = LayerMask.LayerToName(other.gameObject.layer);
if (other.gameObject.tag == "Backpack")
{
// ぶつかったゲームオブジェクト(かばんを想定)を親ゲームオブジェクトにする
this.transform.SetParent(other.gameObject.transform);
// Rigidbodyを取得し、ゲームオブジェクトが物理設定で動かないようにする
Rigidbody rb = this.transform.GetComponent<Rigidbody>();
rb.isKinematic = true;
}
}
// ゲームオブジェクトがプレイヤーに握られたとき
public void ExitGrab()
{
// 親ゲームオブジェクトを空にして独立させる
this.gameObject.transform.parent = null;
// Rigidbodyを取得し、ゲームオブジェクトが物理設定で動くようにする
Rigidbody rb = this.transform.GetComponent<Rigidbody>();
rb.isKinematic = false;
}
}
おわりに
この記事では道具を持ち運ぶ方法について解説しましたが、さらなる手段として