OculusTouch対応のゲームを作る手順目次はこちら
#概要
The Lab等のVRゲームでよくある弓の実装方法を手順を追って説明します。
#完成図
##弓に利用したアセットは以下の通りです。
#考え方
弓に必要な機能を分解すると以下の通りになります。
- 弓本体を拾って持つ機能
- 弓を引いて矢を放つまでの一連の流れのコントロール
- 弓のモデルのrigをコントロールして弓を引くアニメーションを行う
- 矢の機能(当たって止まる、発射した後の矢じりが落ちる方向を向く)
それぞれ順を追って説明します。
#弓本体を拾って持つ機能
1は以前の記事で紹介した物をキャッチするTouchObjectの機能がそのまま使えそうです。弓の本体から手に入れたモデルに対して、RigitBodyとColliderとTouchObjectをアタッチします。TouchObjectをアタッチすればキャッチして持つ事が出来ます。
#弓を引いて矢を放つまでの一連の流れのコントロール
BowObjectというクラスがその働きをします。BowObjectクラスは後述で説明するクラスの全てをコントロールします。機能の一連の流れは下記のようになっています。
- 弓をキャッチしたら弓を引けるようにする
- キャッチした瞬間に矢をインスタンス化する
- GrabObjectを掴むと矢を弓にセットする
- GrabObjectを掴んだ手を引くと、矢を引くアニメーションが動作する
- GrabObjectを離すと矢が発射される
usi System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UniRx;
using UniRx.Triggers;
/// <summary>
/// 弓矢
/// </summary>
[RequireComponent (typeof(TouchObject))]
public class BowObject : MonoBehaviour
{
/// <summary>
/// 矢のPrefab
/// </summary>
[SerializeField] GameObject m_arrowPrefab;
/// <summary>
/// 矢を引き始める位置
/// </summary>
[SerializeField] GrabObject m_arrawGrabObject;
/// <summary>
/// 矢の場所
/// </summary>
[SerializeField] Transform m_arrowRoot;
BoolReactiveProperty m_isShotEnd = new BoolReactiveProperty (false);
BowAnimationCont m_bowAnime;
ArrowObject m_arrowObject;
GameObject m_arrawGameObject;
TouchObject m_touch;
void Start ()
{
m_touch = GetComponent<TouchObject> ();
m_touch.IsCatch.Where (p => p).Subscribe (p => CreateArrow ());
m_isShotEnd.Where (p => p).Delay (System.TimeSpan.FromMilliseconds (500)).Subscribe (p => CreateArrow ());
m_touch.IsCatch.Where (p => !p).Subscribe (p => ResetGrab ());
m_bowAnime = GetComponent<BowAnimationCont> ();
m_arrawGrabObject.IsGrab.Where (p => p).Subscribe (p => ChangeColor (Color.red));
m_arrawGrabObject.IsGrab.Where (p => !p).Subscribe (p => ChangeColor (Color.white));
}
/// <summary>
/// Resets the grab.
/// </summary>
void ResetGrab ()
{
StopCoroutine (GetPowerCoroutine ());
HandControllerManager.Instance.SetIsLockRaycaster (false, false);
if (m_arrawGameObject != null) {
Destroy (m_arrawGameObject);
m_arrawGameObject = null;
}
}
/// <summary>
/// 矢を生成する
/// </summary>
void CreateArrow ()
{
m_isShotEnd.Value = false;
var isLeftLock = m_touch.IsLockHand == TouchObject.LockHand.Left;
HandControllerManager.Instance.SetIsLockRaycaster (isLeftLock, !isLeftLock);
InstanceArrow (isLeftLock);
//Debug.Log (bowLock);
StartCoroutine (GetPowerCoroutine ());
}
/// <summary>
/// インスタンス化
/// </summary>
/// <param name="isLeft">If set to <c>true</c> is left.</param>
void InstanceArrow (bool isLeft)
{
if (m_arrawGameObject == null) {
m_arrawGameObject = GameObject.Instantiate (m_arrowPrefab) as GameObject;
SetParent (m_arrawGameObject.transform, HandControllerManager.Instance.GetHand (isLeft));
m_arrawGameObject.transform.localRotation = Quaternion.Euler (new Vector3 (0, 0, 0));
m_arrowObject = m_arrawGameObject.GetComponent<ArrowObject> ();
}
}
void SetParent (Transform child, Transform parent)
{
child.parent = parent;
child.localPosition = Vector3.zero;
child.localScale = Vector3.one;
child.localRotation = Quaternion.identity;
}
/// <summary>
/// 矢を引いて放つ
/// </summary>
/// <returns>The power coroutine.</returns>
IEnumerator GetPowerCoroutine ()
{
var wait = new WaitForEndOfFrame ();
while (true) {
if (m_arrawGrabObject.IsGrab.Value)
break;
yield return wait;
}
SetParent (m_arrawGameObject.transform, this.m_arrowRoot);
m_arrawGrabObject.InGrabCB = MoveAllowCB;
while (true) {
if (!m_arrawGrabObject.IsGrab.Value) {
break;
}
yield return wait;
}
var body = GetComponent<Collider> ();
body.enabled = false;
var grab = m_arrawGrabObject.gameObject.GetComponent<Collider> ();
grab.enabled = false;
m_arrawGrabObject.InGrabCB = null;
m_arrowObject.Shot (new Vector3 (0, 0, GetPower ().magnitude * 1000));
m_arrowObject.transform.parent = null;
m_bowAnime.Reset ();
m_arrawGameObject = null;
yield return new WaitForSeconds (0.1f);
grab.enabled = true;
body.enabled = true;
ResetGrab ();
m_isShotEnd.Value = true;
}
/// <summary>
/// 矢を放つ力を計算
/// </summary>
/// <returns>The power.</returns>
Vector3 GetPower ()
{
var isLeft = m_touch.IsLeftHand;
var bowGrabPos = Vector3.zero;
var arrowHandPos = Vector3.zero;
if (isLeft) {
bowGrabPos = HandControllerManager.Instance.rightHand.transform.position;
arrowHandPos = HandControllerManager.Instance.leftHand.transform.position;
} else {
bowGrabPos = HandControllerManager.Instance.leftHand.transform.position;
arrowHandPos = HandControllerManager.Instance.rightHand.transform.position;
}
return bowGrabPos - arrowHandPos;
}
/// <summary>
/// 矢を引く際に矢が動く時、マイフレーム実行されるCB
/// </summary>
/// <param name="arrowGrabPosition">Arrow grab position.</param>
void MoveAllowCB (Vector3 arrowGrabPosition)
{
var isLeft = m_touch.IsLeftHand;
var distance = (m_arrawGrabObject.transform.position - arrowGrabPosition).magnitude;
m_arrowObject.transform.localPosition = new Vector3 (0, 0, -distance);
m_bowAnime.DoYumiHiki (distance);
}
void ChangeColor (Color color)
{
foreach (var one in this.GetComponentsInChildren<Renderer>()) {
one.materials [0].color = color;
}
}
}
#弓のモデルのrigをコントロールして弓を引くアニメーションを行う
弓矢の弓の部分にrigが入っていて、rigの座標を動かせば弓を引くアニメーションができる物が必要です。1から作るのも手ですが、フリーで使える弓の本体があったので今回はこれを利用しています。
上記の画像のGreen_Arrow_Bow以下がアセットで、Armature以下がRigになります。弓のRigはStringになり、このY座標を動かす事で弓を引くアニメーションができます。
##Stringオブジェクトのy座標をコントロールするスクリプト
BowAnimationCont.csにてコントロールします。このクラスでは弓を引く距離を指定するとStringオブジェクトのy座標を動かします。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 弓をひくアニメーションをコントロール
/// </summary>
public class BowAnimationCont : MonoBehaviour
{
/// <summary>
/// アニメーションをするRig
/// </summary>
[SerializeField] Transform m_targetBone;
Vector3 m_localstartPos;
void Start ()
{
m_localstartPos = m_targetBone.localPosition;
}
/// <summary>
/// 弓を引く距離を指定する。距離はワールド座標系での距離
/// </summary>
/// <param name="worldDistance">World distance.</param>
public void DoYumiHiki (float worldDistance)
{
m_targetBone.localPosition =
new Vector3 (
m_targetBone.localPosition.x,
m_localstartPos.y + worldDistance * (1f / m_targetBone.lossyScale.y),
m_targetBone.localPosition.z);
}
/// <summary>
/// 弓の位置を元に戻す
/// </summary>
public void Reset ()
{
m_targetBone.transform.localPosition = m_localstartPos;
}
}
#弓を引く
片方の手で弓をキャッチした後で、もう片方の手で弓を引く操作をするするイメージです。下記のGrabPositionオブジェクトにGrabObject.csとSphereColliderがアタッチされてますが、これを以前の記事で作成したHandRaycasterで掴み、掴んだ後のOculusTouchコントローラの移動量を弓を引くためのアニメーションに利用したり、弓をひく動きと矢の移動とを連動させたりします。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UniRx;
using UniRx.Triggers;
using System;
/// <summary>
/// コントローラーで掴むObject
/// </summary>
public class GrabObject : EventBase
{
/// <summary>
/// 掴んだかどうか
/// </summary>
public BoolReactiveProperty IsGrab = new BoolReactiveProperty (false);
/// <summary>
/// 掴んでいる最中のCB。掴んだ手の位置が引数に入る
/// </summary>
/// <value>The m in grab C.</value>
public Action<Vector3> InGrabCB {
get;
set;
}
// Use this for initialization
void Start ()
{
IsGrab.Value = false;
}
public override void OnStartTouchEvent (HandRaycaster hitEvent)
{
base.OnStartTouchEvent (hitEvent);
Debug.Log ("OnGrab");
IsGrab.Value = true;
}
public override void OnInTouchEvent (HandRaycaster hitEvent)
{
base.OnInTouchEvent (hitEvent);
if (HandControllerManager.Instance.IsGetDownIndexTrigger (hitEvent.isLeftHand)) {
} else if (HandControllerManager.Instance.IsGetHandTrigger (hitEvent.isLeftHand)) {
InGrabCB (hitEvent.transform.position);
}
}
public override void OnEndTouchEvent (HandRaycaster hitEvent)
{
base.OnEndTouchEvent (hitEvent);
IsGrab.Value = false;
}
}
#矢の機能(当たって止まる、発射した後の矢じりが落ちる方向を向く)
矢の本体の実装です。下記のコメントを見れば大体機能が分かると思います。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using UniRx;
using UniRx.Triggers;
/// <summary>
/// 矢
/// </summary>
public class ArrowObject : MonoBehaviour
{
Collider m_body;
Rigidbody m_rigit;
void Awake ()
{
m_rigit = GetComponent<Rigidbody> ();
m_rigit.isKinematic = true;
m_rigit.useGravity = false;
m_body = GetComponent<Collider> ();
m_body.enabled = false;
//何かに衝突したら遅れて削除される
this.OnCollisionEnterAsObservable ()
.Delay (TimeSpan.FromMilliseconds (3000.0))
.Subscribe (p => Destroy (gameObject)).AddTo (gameObject);
//何かに刺さったら止まる
this.OnCollisionEnterAsObservable ()
.Subscribe (p => Stop (p.transform));
}
/// <summary>
/// 矢を放つ
/// </summary>
/// <param name="power">Power.</param>
public void Shot (Vector3 power)
{
Debug.Log ("Arrow Shot");
m_body.enabled = true;
m_rigit.isKinematic = false;
m_rigit.useGravity = true;
m_rigit.AddRelativeForce (power);
this.FixedUpdateAsObservable ().Subscribe (p => FallArrowTask ()).AddTo (gameObject);
}
/// <summary>
/// 矢が刺さって止まる
/// </summary>
/// <param name="target">Target.</param>
void Stop (Transform target)
{
m_body.enabled = false;
var ver = m_rigit.velocity;
m_rigit.velocity = Vector3.zero;
m_rigit.useGravity = false;
m_rigit.isKinematic = true;
//少々突き刺ささる
m_rigit.transform.position += ver * Time.fixedDeltaTime * 2;
this.transform.transform.SetParent (target);
}
/// <summary>
/// 矢じりを落ちる方向に向ける
/// </summary>
void FallArrowTask ()
{
if (!m_rigit.isKinematic) {
var ver = m_rigit.velocity;
var falldir = ver.normalized;
//落ちる方向を向くようにする
m_rigit.MoveRotation (Quaternion.LookRotation (falldir));
}
}
}
#まとめ
長かったですが、以上が弓矢の実装の説明になります。あとはSEとOculusTouchの振動を加えればもっと良くなると思います。