#はじめに
こんにちは、ZeniZeniです。
みなさんSteamVRのバージョンは何を使っていますか?SteamVRはバージョンが2.0になってから大幅に仕様が変わり、それを忌避して今もver1.xを使っている人は多いんじゃないんでしょうか。(僕もそうでした)
**しかし!**SteamVR2.0にはめちゃくちゃ便利なコンポーネント群があって、SteamVRを入れるだけで基本的なVRゲームのシステムを実装できます。
この記事では、SteamVR2.0での主要な便利コンポーネントの使い方を紹介していきます。(めちゃくちゃ種類があるので、少しずつ紹介を増やしていきます。)
こちらの公式ドキュメントにも詳しいことが載っているので、ぜひ読んでみてください。
コントローラのinput周りの仕様(binding UI)については、これまでにたくさん記事が出ているので、そちらを参照してください。
フレームシンセシスさんのサイト がわかりやすくておすすめです。
覚えておくべき単語
それからSteamVRにはあまり馴染みのない単語があって少々混乱するので、それらをここで紹介します。
名前 | 役割 |
---|---|
Skeleton | 骸骨・骨組みという単語ですが、SteamVRでは手のモデル(5本指に限らない)をSkeletonと呼びます。またViveコントローラのモデルをControllerと呼び、ControllerとSkeletonを二つ合わせてHandと呼びます。 |
Hover | SteamVR独自の当たり判定のようなものです。空中に停止する・漂うという単語ですが、インタラクション対象に触れている状態を意味します。名詞的にコリダーと同様の意味でも使われます。 |
Snap | 音を鳴らす・かみつくという単語ですが、オブジェクトを手で掴む際の、オブジェクトを手に引き寄せる行為を意味します。snap the object to the center of the handみたいな感じで出てきます。 |
#で、どう便利なんですか?
まずはSteamVR2.0を導入して、フレームシンセシスさんのサイトに書いてある通りに設定をしましょう。
その後、Asset/SteamVR/InteractionSystem/Samples/Interactions_Example.sceneを開きましょう。
するとこのようなシーンが開きます。
このシーンを起動すると、テレポート移動しながら、Steamが用意してくれる多様なInteraction Systemを体験できます。
もうここだけでも1時間は遊べますね!
このシーンからわかる通り、SteamVRには基本的な「物をつかむ」、「物を投げる」、「テレポートする」以外の多様な機能が標準実装されています。
さらにこれらはVRでなくても遊べるようになっているのがすごいんです。
SteamVR2.0にはfallbackという機能が付いていて、下図の2D Debugを押すとキーボードとマウスでVRと同様のことができます。
チーム開発でVR環境のない人もデバッグができるのは素晴らしいですね。
主要なコンポーネント
基本的には、Playerコンポーネントを持つクラスが存在し、インタラクション対象にはInteractableコンポーネントを付け、コントローラにはHandコンポーネントを付けます。
SteamVRのInteraction Systemはこれらを参照して、様々な命令を行っています。
Player.prefabがCore/Prefabsにあるので、それを使うのが簡単です。これはCameraRigのより発展的な物で、InteractiveSystemを使うために必要な設定がされています。
インスペクターの項目が多すぎてうへーっってなりますが、項目をひとつずつ簡単に説明します。
Player.cs
シングルトン的な扱いになっているので、シーンに二つ以上置けません。
GetComponentしなくてもどこからでも呼び出せます。(便利!)
基本的には他のクラスはこのPlayerコンポーネントを利用して、HandやCameraを参照します。
特にこのインスペクターをいじるということは少ないと思います。
##Interactable.cs
インタラクションしたいオブジェクトには必ずこのコンポーネントを付けてください。
このコンポーネントの様々なメソッドを利用して、複雑なインタラクションシステムを構成していきます。
変数名 | 型 | 役割 |
---|---|---|
Activate Action Set On Attach | SteamVR_ActionSet | SteamVR Inputに登録したAction Setを有効にします。 自前で作成したAction Setで操りたい場合はここにそのAction Setを設定します。 |
Hide Hand On Attach | bool | trueだと物を掴んだりするとき(Hand.AttachObject()が呼ばれるとき)に、コントローラも手のモデルも消します。逆に物を離す時(Hand.DettachObject()が呼ばれるとき)には出現します。 |
Hide Skeleton On Attach | bool | Hide Hand On Attachと違い手だけが消えるようになります。 |
Hide Skeleton On Attach | bool | Hide Hand On Attachと違いコントローラだけが消えるようになります。 |
Hand Animation On Pickup | int | 物を掴む際の手のアニメーションを指定します。手のアニメーターコントローラはint型のanimationStateという変数でアニメーションを切り替えていて、指定した値のアニメーションが物を掴む際に実行されます(後述)。 ...のはずなんですけど0以外を指定するとエラーがめちゃめちゃ出る。というか手の形を指定する場合はSteamVR_Skeleton_Poserを使うので(後述)、これどう使うのかわからない。サンプルはみんな0だし。 |
Set Range Of Motion On Pickup | SkeletalMotionRangeChange | 手の形をコントローラを握ってる形に合わせるか合わせないかを決めます。どうでもいいならNoneにします。 |
Use Hand Object Attachment Point | bool | オブジェクトを掴む際の手の位置を、Hand.ObjectAttachmentPointのTransformを参照するかHand自体のTransformを参照するかを決めます。SteamVR_Skeleton_Poserで手の位置と形を指定しているとfalseになることに注意。 |
Attach Ease In | bool | 物を掴む際に、オブジェクトがゆっくり手に収まるか、即座に手に収まるかを決めます。 |
Snap Attach Ease In Time | float | Attach Ease In がtrueのとき、オブジェクトが手に収まるまでの時間を指定します。 |
Snap Attach Ease In Completed | bool | Attach Ease In がtrueになっていて、オブジェクトがゆっくり移動している間はfalse。移動が完了するとtrueになります。それ以外の時にtrueになっているかfalseになっているかは不定(確認用だからいじることはない) |
Hand Follow Transform | bool | falseのとき、物を掴むと、掴んだ瞬間のオブジェクトと手の相対距離のまま、オブジェクトが手に追従します。trueならばUse Hand Object Attachment Pointで指定したTransformの位置に手が来ます。SteamVR_Skeleton_Poserで手の位置と形を指定しているとtrue、false関係なくSteamVR_Skeleton_Poserで指定した手と位置と形になります。 |
Highlight On Hover | bool | Handがオブジェクトに触れているときに、オブジェクトにハイライトを付けるかを決めます。 |
Hide Highlight | GameObject[] | ここに設定された子オブジェクトはHighlightされなくなります。 |
Hand.cs
コントローラにつけるコンポーネントですね。
主要な変数
それから頻繁に使われる変数を紹介します。
AttachmentFlags
Hand.csが持つ列挙型の変数です。掴む際の諸々の挙動を指定します。現状8種類存在していてビットフィールドで管理されています。
そのため初期化時には
private Hand.AttachmentFlags attachmentFlags = Hand.defaultAttachmentFlags & ( ~Hand.AttachmentFlags.SnapOnAttach ) & (~Hand.AttachmentFlags.DetachOthers) & (~Hand.AttachmentFlags.VelocityMovement);
というように論理演算子が使われています。
名前 | ビットフィールド | 役割 |
---|---|---|
Snap On Attach | 00000001 | これを指定しておかないと、Interactable.csのAttachEaseInとHandFollowTransformが常にfalseのような挙動をします。 |
Detach Others | 00000010 | 物を掴む際に、既に掴んでいるオブジェクトがあればすべて離します。 |
Detach From OtherHand | 00000100 | 物を掴む際に、反対の手で掴んでいるオブジェクトも離します。 |
Parent To Hand | 00001000 | 物を掴む際に、オブジェクトを手の子オブジェクトにします。 |
VelocityMovement | 00010000 | 掴んだオブジェクトが他のコリダーを絶対に貫通しないようになります。そのため、下のTuen On Kinematicと一緒に指定するとコリダーを貫通するようになり、指定する意味がなくなります。これを指定するオブジェクトは必ずRigidbodyを持つ必要があります。 |
Turn On Kinematic | 00100000 | これを指定しているオブジェクトは必ずRigidbodyを持つ必要があり、掴んだ際にRigidbody.IsKinemeticがtrueになります。 |
Turn On Gravity | 01000000 | これを指定しているオブジェクトは必ずRigidbodyを持つ必要があり、掴んだ際にRigidbody.UseGravityがtrueになります。 |
Allow Sidegrade | 10000000 | どこにも使われていないのでよくわかっていないです。コメントではThe object is able to switch from a pinch grab to a grip grab. Decreases likelyhood of a good throw but also decreases likelyhood of accidental dropと書いてあります。 |
Hand.defaultAttachmentFlagsでは下のようになっています。
public const AttachmentFlags defaultAttachmentFlags = AttachmentFlags.ParentToHand |
AttachmentFlags.DetachOthers |
AttachmentFlags.DetachFromOtherHand |
AttachmentFlags.TurnOnKinematic |
AttachmentFlags.SnapOnAttach;
GrabTypes
物を掴む際にどのボタンで掴むかを決めるための列挙型の変数です。None, Trigger, Pinch, Grip, Scripted
の5種類が存在します。
詳細は後述します。
#インタラクションする方法
インスペクターに出てる機能を理解したところで、さっそくインタラクションできるオブジェクトを作ってみましょう。
SteamVRのInteractionシステムは、Handコンポーネントが、Interactiveコンポーネントに自身の状態(オブジェクトに触れているかどうか等)を伝えることで、様々な処理を行っています。
基本的なイベント
まず基本的な関数を紹介します。これらは特定の条件下で自動的に呼ばれます。
void OnHandHoverBegin(Hand hand)
Hover(SteamVR独自の当たり判定)がオブジェクトに触れた瞬間に1度だけ呼ばれます。
OnColliderEnterみたいなものですね。
void OnHandHoverEnd(Hand hand)
Hoverがオブジェクトから離れた瞬間に1度だけ呼ばれます。
void OnHandHoverStay(Hand hand)
Hoverがオブジェクトに触れている間常に呼ばれます。
void OnAttachedToHand(Hand hand)
Hand.AttachObjectでこのオブジェクトがAttachされたときに1度だけ呼ばれます。
void OnDetachedFromHand(Hand hand)
Hand.DetachObjectでこのオブジェクトがDetachされたときに1度だけ呼ばれます。
void HandAttachedUpdate(Hand hand)
オブジェクトがアタッチされている間常に呼ばれます。
void OnHandFocusAcquired(Hand hand)
前提としてオブジェクトは複数アタッチでき、GameObjectのリストattachedobjectsに順にaddされるのですが、一番最後にアタッチされたオブジェクトはHand.currentAttachedObjectで取得できます。このcurrentAttachObjectがDetachされたとき、その一つ前にAttachされたオブジェクトで1度だけ呼ばれます。
void OnHandFocusLost(Hand hand)
新しくオブジェクトがAttachされるとき、その時点で一番新しくAttachされたオブジェクト(currentAttachObject)で1度だけ呼ばれます。
その他のよく使われる関数
そのほかに、頻繁に使われる便利な関数とその使い方を紹介します。
void Hand.HoverLock(Interactable interactable)
この関数を呼ぶと、引数に入れたInteractableを持つオブジェクトでしか上記の関数が呼ばれなくなります。
オブジェクトを掴んでいる間は他のオブジェクトに干渉したくないときに使います。
void Hand.HoverUnlock(Interactable interactable)
HoverLockでの処理をなかったことにし、他のオブジェクトでも上記の関数が呼ばれるようになります。
うっかり呼び忘れると何も反応しなくなります。
GrabTypes Hand.GetGrabStarting(GrabTypes explicitType = GrabTypes.None)
SteamVR2.0では物を掴む時の入力はトリガーかグリップと設定しています。
この関数は二種類の入力の始まり(ボタンを押す)を検知する関数です。
引数に何も渡さないと、トリガーが押されるとGrabTypes.Pinch、グリップが押されるとGrabTypes.Gripを1度だけ返します。どちらでもなければGrabTypes.Noneを返します。。
引数にGrabTypes.Pinchが渡されていれば、トリガーが押されるとGrabTypes.Pinchを1度だけ返し、それ以外の時はGrabTypes.Noneを返します。
引数にGrabTypes.Gripが渡されていれば、グリップが押されるとGrabTypes.Gripを1度だけ返し、それ以外の時はGrabTypes.Noneを返します。
GrabTypes Hand.GetGrabEnding(GrabTypes explicitType = GrabTypes.None)
GetGrabEndingは、GetGrabStartingの入力のおわり(ボタンを離す)版です。
##使い方
例として、SteamVR側がInteractableExample.csを作成しており、それを参考にするとよいと思います。
//======= Copyright (c) Valve Corporation, All rights reserved. ===============
//
// Purpose: Demonstrates how to create a simple interactable object
//
//=============================================================================
using UnityEngine;
using System.Collections;
namespace Valve.VR.InteractionSystem.Sample
{
//-------------------------------------------------------------------------
[RequireComponent( typeof( Interactable ) )]
public class InteractableExample : MonoBehaviour
{
private TextMesh generalText;
private TextMesh hoveringText;
private Vector3 oldPosition;
private Quaternion oldRotation;
private float attachTime;
private Hand.AttachmentFlags attachmentFlags = Hand.defaultAttachmentFlags & ( ~Hand.AttachmentFlags.SnapOnAttach ) & (~Hand.AttachmentFlags.DetachOthers) & (~Hand.AttachmentFlags.VelocityMovement);
private Interactable interactable;
//-------------------------------------------------
void Awake()
{
var textMeshs = GetComponentsInChildren<TextMesh>();
generalText = textMeshs[0];
hoveringText = textMeshs[1];
generalText.text = "No Hand Hovering";
hoveringText.text = "Hovering: False";
interactable = this.GetComponent<Interactable>();
}
//-------------------------------------------------
// Called when a Hand starts hovering over this object
//-------------------------------------------------
private void OnHandHoverBegin( Hand hand )
{
generalText.text = "Hovering hand: " + hand.name;
}
//-------------------------------------------------
// Called when a Hand stops hovering over this object
//-------------------------------------------------
private void OnHandHoverEnd( Hand hand )
{
generalText.text = "No Hand Hovering";
}
//-------------------------------------------------
// Called every Update() while a Hand is hovering over this object
//-------------------------------------------------
private void HandHoverUpdate( Hand hand )
{
GrabTypes startingGrabType = hand.GetGrabStarting();//vive controllerの場合、トリガーかグリップボタンを押すとそれぞれ、Pinch、Gripになります。
bool isGrabEnding = hand.IsGrabEnding(this.gameObject);//このオブジェクトをAttachした状態で、トリガーかグリップボタンを押すのをやめるとtrueになります。
if (interactable.attachedToHand == null && startingGrabType != GrabTypes.None)
{
// Save our position/rotation so that we can restore it when we detach
oldPosition = transform.position;
oldRotation = transform.rotation;
// Call this to continue receiving HandHoverUpdate messages,
// and prevent the hand from hovering over anything else
hand.HoverLock(interactable);
// Attach this object to the hand
hand.AttachObject(gameObject, startingGrabType, attachmentFlags);
}
else if (isGrabEnding)
{
// Detach this object from the hand
hand.DetachObject(gameObject);
// Call this to undo HoverLock
hand.HoverUnlock(interactable);
// Restore position/rotation
transform.position = oldPosition;
transform.rotation = oldRotation;
}
}
//-------------------------------------------------
// Called when this GameObject becomes attached to the hand
//-------------------------------------------------
private void OnAttachedToHand( Hand hand )
{
generalText.text = string.Format("Attached: {0}", hand.name);
attachTime = Time.time;
}
//-------------------------------------------------
// Called when this GameObject is detached from the hand
//-------------------------------------------------
private void OnDetachedFromHand( Hand hand )
{
generalText.text = string.Format("Detached: {0}", hand.name);
}
//-------------------------------------------------
// Called every Update() while this GameObject is attached to the hand
//-------------------------------------------------
private void HandAttachedUpdate( Hand hand )
{
generalText.text = string.Format("Attached: {0} :: Time: {1:F2}", hand.name, (Time.time - attachTime));
}
private bool lastHovering = false;
private void Update()
{
if (interactable.isHovering != lastHovering) //save on the .tostrings a bit
{
hoveringText.text = string.Format("Hovering: {0}", interactable.isHovering);
lastHovering = interactable.isHovering;
}
}
//-------------------------------------------------
// Called when this attached GameObject becomes the primary attached object
//-------------------------------------------------
private void OnHandFocusAcquired( Hand hand )
{
}
//-------------------------------------------------
// Called when another attached GameObject becomes the primary attached object
//-------------------------------------------------
private void OnHandFocusLost( Hand hand )
{
}
}
}
長くなってしまったので、ボタンや弓矢といったInteractionシステムは別の記事で紹介します。
#開発、執筆にあたり、下記のサイト様を参考、引用させていただきました。